Simple mathematical captcha where human is asked to solve a simple mathematical calculation like 2+3=?. Don't require database access or external libraries like PIL.
To use MathCaptchaForm subclass it and add your fields.
For example of usage and discussion about security of this captcha comparing with traditional image captcha please see my blog entry Improved Mathematical Captcha
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | #!/usr/bin/env python
# -*- coding:utf-8 -*-
# Copyright (c) 2007, Dima Dogadaylo (www.mysoftparade.com)
import re
import sha
import pickle
import base64
import time
from random import randint
from django import newforms as forms
from django.conf import settings
class MathCaptchaForm(forms.Form):
"""Lightweight mathematical captcha where human is asked to solve
a simple mathematical calculation like 3+5=?. It don't use database
and don't require external libraries.
From concatenation of time, question, answer, settings.SITE_URL and
settings.SECRET_KEY is built hash that is validated on each form
submission. It makes impossible to "record" valid captcha form
submission and "replay" it later - form will not be validated
because captcha will be expired.
For more info see:
http://www.mysoftparade.com/blog/improved-mathematical-captcha/
"""
A_RE = re.compile("^(\d+)$")
captcha_answer = forms.CharField(max_length = 2, required=True,
widget = forms.TextInput(attrs={'size':'2'}))
captcha_token = forms.CharField(max_length=200, required=True,
widget=forms.HiddenInput())
def __init__(self, *args, **kwargs):
"""Initalise captcha_question and captcha_token for the form."""
super(MathCaptchaForm, self).__init__(*args, **kwargs)
# reset captcha for unbound forms
if not self.data:
self.reset_captcha()
def reset_captcha(self):
"""Generate new question and valid token
for it, reset previous answer if any."""
q, a = self._generate_captcha()
expires = time.time() +\
getattr(settings, 'CAPTCHA_EXPIRES_SECONDS', 60*60)
token = self._make_token(q, a, expires)
self.initial['captcha_token'] = token
self._plain_question = q
# reset captcha fields for bound form
if self.data:
def _reset():
self.data['captcha_token'] = token
self.data['captcha_answer'] = ''
if hasattr(self.data, '_mutable') and not self.data._mutable:
self.data._mutable = True
_reset()
self.data._mutable = False
else:
_reset()
def _generate_captcha(self):
"""Generate question and return it along with correct answer."""
a, b = randint(1,9), randint(1,9)
return ("%s+%s" % (a,b), a+b)
def _make_token(self, q, a, expires):
data = base64.urlsafe_b64encode(\
pickle.dumps({'q': q, 'expires': expires}))
return self._sign(q, a, expires) + data
def _sign(self, q, a, expires):
plain = [getattr(settings, 'SITE_URL', ''), settings.SECRET_KEY,\
q, a, expires]
plain = "".join([str(p) for p in plain])
return sha.new(plain).hexdigest()
@property
def plain_question(self):
return self._plain_question
@property
def knotty_question(self):
"""Wrap plain_question in some invisibe for humans markup with random
nonexisted classes, that makes life of spambots a bit harder because
form of question is vary from request to request."""
digits = self._plain_question.split('+')
return "+".join(['<span class="captcha-random-%s">%s</span>' %\
(randint(1,9), d) for d in digits])
def clean_captcha_token(self):
t = self._parse_token(self.cleaned_data['captcha_token'])
if time.time() > t['expires']:
raise forms.ValidationError("Captcha is expired.")
self._plain_question = t['q']
return t
def _parse_token(self, t):
try:
sign, data = t[:40], t[40:]
data = pickle.loads(base64.urlsafe_b64decode(str(data)))
return {'q': data['q'],
'expires': float(data['expires']),
'sign': sign}
except Exception, e:
import sys
sys.stderr.write("Captcha error: %r\n" % e)
raise forms.ValidationError("Invalid captcha!")
def clean_captcha_answer(self):
a = self.A_RE.match(self.cleaned_data.get('captcha_answer'))
if not a:
raise forms.ValidationError("Number is expected!")
return int(a.group(0))
def clean(self):
"""Check captcha answer."""
cd = self.cleaned_data
# don't check captcha if no answer
if 'captcha_answer' not in cd:
return cd
t = cd.get('captcha_token')
if t:
form_sign = self._sign(t['q'], cd['captcha_answer'],
t['expires'])
if form_sign != t['sign']:
self._errors['captcha_answer'] = ["Are you human?"]
else:
self.reset_captcha()
return super(MathCaptchaForm, self).clean()
|
More like this
- Template tag - list punctuation for a list of items by shapiromatron 10 months, 2 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 3 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
- Help text hyperlinks by sa2812 1 year, 7 months ago
Comments
I mentioned the same thing on your blog, but you should probably do some checking to make sure that the captcha question has not been changed during the post.
You could solve the problem by making adding hash of the values concatenated with your settings.SECRET_KEY, maybe even additional data as well, which is similar to how the comments app works to begin with. Just toss the hash in a hidden field and compare the hash to the forms to confirm that the initial question remained the same between display and post.
Should be relatively easy to implement.
#
As we discussed on your blog, what I'm suggesting doesn't solve the problem of replay attacks, but at least means that each attack has to be customized for a particular site.
#
#
Please login first before commenting.