Another captcha implementation for Flask and WTForms
Let’s burn their deeplearning GPU’s.
In the past I wrote a captcha in PHP to limit email newsletter sign ups, worked fine, in fact it is still in use today. You cannot really block spam registrations. There are registration robots but there are also people being paid a few bucks to flood your website with fake or troll accounts. That is the reality and we have to face it. And now there is also deeplearning that can be used to break our captcha code in just 15 minutes.
We, the people developing websites must come up with ideas to challenge fake registrations. It is not possible but few things can be done and adding a captcha is one of them.
For many probably boring stuff, you can find enough libraries doing this for you. Me, I do not want to use a library, I want to write my own captcha in Python, learn baby. And because I value the privacy of the visitors of my websites I also do not want to use ReCaptcha or another remotely served solution.
The captcha solution presented below is working but not finished, just basic, distortion must be added. It is not really Flask specific, it's just Python. I used BytesIO for the first time to save an image not to a file but to memory. What is Flask specific is how to display it in a signup form and in a web page. Note that this is not a complete solution, it just details the most important parts.
Here is how the captcha is generated. captcha_fonts_path() is a function you need to make to import the font(s) for the captcha.
import math
import random
import secrets
from PIL import Image, ImageFont, ImageDraw, ImageOps
from io import BytesIO
def captcha_create():
use_chars = ['A', 'b', 'C', 'd', 'e', 'f', 'h', 'K', 'M', 'N', 'p', 'R', 'S', 'T', 'W', 'X', 'Y', 'Z']
use_nums = ['2', '3', '4', '6', '7', '8', '9']
code_char_count = 5
code_chars = []
for i in range(code_char_count):
s = random.randrange(2)
if s & 1:
code_chars.append( random.choice(use_chars) )
else:
code_chars.append( random.choice(use_nums) )
# captcha dimensions
w = 300
h = 100
background_color = (255, 255, 255)
im_captcha = Image.new('RGB', (w, h), background_color)
# equal width pieces for each code char
w_code_char = int(math.floor(w/code_char_count))
h_code_char = h
ttf_file = os.path.join(captcha_fonts_path(), 'Arial.ttf')
font_size = int(w_code_char * 3/4)
draw_x = int(w_code_char/4)
draw_y = int(h/4)
# draw code chars one by one at different angle (with different font)
w_code_char_offset = 0
for code_char in code_chars:
im_font = ImageFont.truetype(ttf_file, font_size)
im_code_char = Image.new('RGB', (w_code_char, h_code_char), background_color)
draw = ImageDraw.Draw(im_code_char)
# see font size
draw.text( (draw_x, draw_y), code_char, fill='black', font=im_font)
# random angle is between -20 - +20
angle = random.randrange(40) - 20
im_code_char_rotated = im_code_char.rotate(angle, fillcolor=background_color)
# paste char image into chars image
im_captcha.paste(im_code_char_rotated, (w_code_char_offset, 0))
w_code_char_offset += w_code_char
# do some line / distortion stuff (to be implemented)
# save (in memory) as jpg
im_captcha_io = BytesIO()
im_captcha.save(im_captcha_io, format='JPEG')
captcha_code = ''.join(code_chars)
captcha_image_data = im_captcha_io.getvalue()
captcha_token = secrets.token_urlsafe()
captcha_token_hashed = hash_string(captcha_token)
return captcha_token, captcha_token_hashed, captcha_code, captcha_image_data
To show the captcha image with Flask in your browser:
@auth.route('/captcha', methods=['GET'])
def captcha():
captcha_token, captcha_token_hashed, captcha_code, captcha_image_data = captcha_create()
resp = make_response(captcha_image_data)
resp.content_type = "image/jpeg"
return resp
Now we should also show the captcha image in a form. I use Flask with Flask-WTForms and have a Jinja macro that puts all the form fields on the page. This saves so much time! To put the captcha image in the form we need a widget. How to do this? I am certainly not a WTForms pro and believe there should be much more examples of widgets. Here is my solution. In forms.py I have created a widget for CaptchaCodeField, it does nothing more than put the image on the screen:
class CaptchaCodeOutput(Input):
def __call__(self, field, **kwargs):
return kwargs.get('value', '<img src="' + field._value() + '" style="border: 1px solid #dddddd; ">')
class CaptchaCodeField(StringField):
widget = CaptchaCodeOutput()
class AuthRegisterForm(FlaskForm):
....
captcha_token = HiddenField('Captcha token')
captcha_image_url = CaptchaCodeField(_l('Captcha image url'))
captcha_code = StringField(_l('Code'), filters=[strip_whitespace], validators=[
DataRequired()])
accept_conditions = BooleanField(_l('I have read and accept the privacy policy *'),validators=[
DataRequired()])
submit = SubmitField(_l('Register'))
def validate_password(self, field):
password = field.data
if not valid_password(password):
raise ValidationError(valid_password_message())
Then in views.py I stuff the captcha image url in this field before calling render_template():
....
captcha_image_url = url_for('auth.captcha', captcha_token=captcha_token)
form.captcha_token.data = captcha_token
form.captcha_image_url.data = captcha_image_url
if captcha_error:
flash(_l('The code you entered did not match the code from the image. Please try again.'))
return render_template(
'auth/register_step2.html',
form=form)
I think it is still worth the trouble to put a captcha on your website if your website visitors do not have to pay for something. Of course we have deeplearning now but the captcha developers toolbox is also more filled today. Think of using different fonts, different width and height fonts, add distortion, reverse colors, be creative, be unpredictable, let's burn their deeplearning GPU's! You may also want to consider other captcha solutions like Q&A.
ImportError: The _imagingft C module is not installed
Of course another nice error message showed up when using Pillow, see above. This can be solved by adding freetype, freetype-dev. You can check this by entering your container and going interactive by running:
phython3
and then typing:
from PIL import features
features.check('freetype2')
If freetype is installed this will return True, if not installed it will return False. Because I am using Docker and the Alpine image I had to make changes to my Dockerfile:
FROM python:3.6-alpine as base
MAINTAINER Peter Mooring peterpm@xs4all.nl peter@petermooring.com
RUN mkdir /svc
WORKDIR /svc
COPY requirements.txt .
# install package dependencies
# COPY requirements.txt /requirements.txt, requirements.txt already copied
# Solve 'No working compiler found' error,
# see: https://github.com/gliderlabs/docker-alpine/issues/458
# Solve 'The headers or library files could not be found for jpeg, a required dependency when compiling Pillow from source.',
# see https://blog.sneawo.com/blog/2017/09/07/how-to-install-pillow-psycopg-pylibmc-packages-in-pythonalpine-image/
# Solve: ImportError: The _imagingft C module is not installed (when using '.paste' of Pillow)
# Solved after adding freetype, freetype-dev
RUN rm -rf /var/cache/apk/* && \
rm -rf /tmp/*
RUN apk update
# Instead, I run python setup.py bdist_wheel first, then run pip wheel -r requirements.txt for pypi packages.
RUN apk add --update \
curl \
python3 \
pkgconfig \
python3-dev \
openssl-dev \
libffi-dev \
musl-dev \
make \
gcc \
freetype \
freetype-dev \
jpeg-dev zlib-dev \
libmagic \
&& rm -rf /var/cache/apk/* \
&& pip wheel -r requirements.txt --wheel-dir=/svc/wheels
# the wheels are now here: /svc/wheels
FROM python:3.6-alpine
RUN apk add --no-cache \
freetype \
freetype-dev \
jpeg-dev zlib-dev \
libmagic
COPY --from=base /svc /svc
WORKDIR /svc
RUN pip install --no-index --find-links=/svc/wheels -r requirements.txt
# after installation, remove wheels, does not free up space, probably because we are in new layer, too bad is some 20MB
#RUN rm -R *
# create and set working directory
RUN mkdir -p /home/flask/app/web
WORKDIR /home/flask/app/web
# copy app code into container
COPY . ./
# create group and user used in this container
RUN addgroup flaskgroup && adduser -D flaskuser -G flaskgroup && chown -R flaskuser:flaskgroup /home/flask
USER flaskuser
Links / credits
How I developed a captcha cracker for my University's website
https://dev.to/presto412/how-i-cracked-the-captcha-on-my-universitys-website-237j
How to break a CAPTCHA system in 15 minutes with Machine Learning
https://medium.com/@ageitgey/how-to-break-a-captcha-system-in-15-minutes-with-machine-learning-dbebb035a710
Recent
- Hiding database UUID primary keys of your web application
- Don't Repeat Yourself (DRY) with Jinja2
- SQLAlchemy, PostgreSQL, maximum number of rows per user
- Show the values in SQLAlchemy dynamic filters
- Secure data transfer with Public Key encryption and pyNaCl
- rqlite: a high-availability and distributed SQLite alternative
Most viewed
- Using Python's pyOpenSSL to verify SSL certificates downloaded from a host
- Using UUIDs instead of Integer Autoincrement Primary Keys with SQLAlchemy and MariaDb
- Using PyInstaller and Cython to create a Python executable
- Connect to a service on a Docker host from a Docker container
- SQLAlchemy: Using Cascade Deletes to delete related objects
- Flask RESTful API request parameter validation with Marshmallow schemas