Django and Captcha images

Check out the new site at https://rkblog.dev.

Captcha are images with some text that user need to write into a form field in order to send the form data. It is used as a anti-spam solutions as bots can't read text from images. In case of django/python the solution is quite easy. We can use code published by various users, for example django-captcha.
Note, that Captcha decrease site usability for people with disabilities so you should use Captcha carefully.

How does Captcha works ?

- We generate a random string and place it on a temporary image
- We show that image in the form
- We make a md5 or sha1 hash out of that string and send it as a hidden field of the form or we use sessions or cookies to store it.
- When user submits the form the text he entered is hashed and compared with the original hash. If they are equal then user entered correct captcha text.

Simple Captcha with PIL

Requirements

- PIL (Python imaging library)
- Simple image (a small blank banner or similar image with non aggressive background)
- A TTF font file - a bit "fantasy" font would be nice (but readable)

The Code

I used blank image called bg.jpg which I placed in the /site_media folder:
I've also placed in the same folder SHERWOOD.TTF a Baldurs Gate like font file. Next I've created a simple project and app, which returned "captcha" view under / root URL.

views.py code:
from django.shortcuts import render_to_response
from os import remove
def captcha(request):
		# random generator
		from random import choice
		# PIL elements, sha for hash
		import Image, ImageDraw, ImageFont, sha
		# create a 5 char random strin and sha hash it, note that there is no big i
		SALT = settings.SECRET_KEY[:20]
		imgtext = ''.join([choice('QWERTYUOPASDFGHJKLZXCVBNM') for i in range(5)])
		# create hash
		imghash = sha.new(SALT+imgtext).hexdigest()
		# create an image with the string (media is the folder with static files accessed by /site_media)
		# PIL "code" - open image, add text using font, save as new
		im=Image.open('media/bg.jpg')
		draw=ImageDraw.Draw(im)
		font=ImageFont.truetype('media/SHERWOOD.TTF', 18)
		draw.text((10,10),imgtext, font=font, fill=(100,100,50))
		# save as a temporary image
		# I use user IP for the filename, SITE_IMAGES_DIR_PATH - system path to folder for images
		temp = settings.SITE_IMAGES_DIR_PATH + request.META['REMOTE_ADDR'] + '.jpg'
		tempname = request.META['REMOTE_ADDR'] + '.jpg'
		im.save(temp, "JPEG")
		
		if request.POST:
			data = request.POST.copy()
			# does the captcha math ?
			if data['imghash'] == sha.new(SALT+data['imgtext']).hexdigest():
				# captcha ok
				# save data etc.
				# use another view/template in render_to_response and delete the temp captcha file:
				#remove(temp)
				return render_to_response('form.html', {'ok': True, 'hash': imghash, 'tempname': tempname})
			else:
				# captcha bad
				# return the form
				return render_to_response('form.html', {'error': True, 'hash': imghash, 'tempname': tempname})
		# no post data, show the form
		else:
			return render_to_response('form.html', {'hash': imghash, 'tempname': tempname})

form.html template code:
{% if error %}
		<div style="text-align:center;"><b>FAILURE</b></div>
	{% endif %}
	{% if ok %}
		<div style="text-align:center;"><b>SUCCESS ! :)</b></div>
	{% endif %}
	<form action="." method="POST"><input type="hidden" value="{{ hash }}" name="imghash">
	<b>Text from the image</b><br />(Use CAPITAL letters)<br />
	<input type="text" size="20" name="imgtext"><br /><img src="/site_media/{{ tempname }}"><br />
	<br /><input type="submit" value="Send Form" class="actiontable">
	</form>
temp is a path + filename of the temporary captcha image. tempname is just the name, which is passed to the template and it is used to show the correct image. On success the view should delete the temporary image (if the user can't use the form again - for example login form)

Result

Django Manipulator with Captcha validation

For old form system (Django <= 0.95) we can create our own Manipulators. A login form with Captcha could look like this:
class LoginForm(forms.Manipulator):
	def __init__(self):
		self.fields = (forms.TextField(field_name="login", length=30, maxlength=200, is_required=True),
		forms.PasswordField(field_name="password", length=30, maxlength=200, is_required=True),
		forms.TextField(field_name="imgtext", is_required=True, validator_list=[self.hashcheck]),
		forms.TextField(field_name="imghash", is_required=True),)
	def hashcheck(self, field_data, all_data):
		import sha
		SALT = settings.SECRET_KEY[:20]
		if not all_data['imghash'] == sha.new(SALT+field_data).hexdigest():
			raise validators.ValidationError("Captcha Error.")

def loginlogout(request):
	from django.contrib.auth import authenticate, login
	if not request.user.is_authenticated():
		temp = settings.SITE_IMAGES_DIR_PATH + request.META['REMOTE_ADDR'] + '.jpg'
		tempname = request.META['REMOTE_ADDR'] + '.jpg'
		# captcha image creation
		from random import choice
		import Image, ImageDraw, ImageFont, sha
		# create a 5 char random strin and sha hash it
		SALT = settings.SECRET_KEY[:20]
		imgtext = ''.join([choice('QWERTYUOPASDFGHJKLZXCVBNM') for i in range(5)])
		imghash = sha.new(SALT+imgtext).hexdigest()
		# create an image with the string
		im=Image.open(settings.SITE_IMAGES_DIR_PATH + '../bg.jpg')
		draw=ImageDraw.Draw(im)
		font=ImageFont.truetype(settings.SITE_IMAGES_DIR_PATH + '../SHERWOOD.TTF', 24)
		draw.text((10,10),imgtext, font=font, fill=(100,100,50))
		im.save(temp,"JPEG")
		
		manipulator = LoginForm()
		# log in user
		if request.POST:
			data = request.POST.copy()
			errors = manipulator.get_validation_errors(data)
			if not errors:
				manipulator.do_html2python(data)
				user = authenticate(username=data['login'], password=data['password'])
				if user is not None:
					login(request, user)
					remove(temp)
					return HttpResponseRedirect("/user/")
				else:
					data['imgtext'] = ''
					form = forms.FormWrapper(manipulator, data, errors)
					return render_to_response('userpanel/' + settings.ENGINE + '/login.html', {'loginform': True, 'error': True, 'hash': imghash, 'form': form, 'theme': settings.THEME, 'engine': settings.ENGINE, 'temp':tempname})
		# no post data, show the login forum
		else:
			errors = data = {}
		form = forms.FormWrapper(manipulator, data, errors)
		return render_to_response('userpanel/' + settings.ENGINE + '/login.html', {'loginform': True, 'hash': imghash, 'form': form, 'theme': settings.THEME, 'engine': settings.ENGINE, 'temp':tempname})
	else:
		# user authenticated
		if request.GET:
			# logout user
			data = request.GET.copy()
			if data['log'] == 'out':
				from django.contrib.auth import logout
				logout(request)
				return HttpResponseRedirect("/user/")
		return HttpResponseRedirect("/user/")
This code also saves captcha images in filenames based on user IP which prevents them from being overwritten by newer request (races) which in some cases can cause problems.

Bots read captchas ?

Captchas aren't perfect and spammers can create tools that read text from images. Some sites use really fuzzy images but they are fuzzy also for users. A possible solution is to use a fantasy-like font which is still readable but doesn't look like a normal font.
RkBlog

Programming in Python, 14 July 2008


Check out the new site at https://rkblog.dev.
Comment article
Comment article RkBlog main page Search RSS Contact