angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Another captcha implementation for Flask and WTForms

Let’s burn their deeplearning GPU’s.

4 July 2019 Updated 29 August 2019
post main image
Original photo unsplash.com/@barnikakovacs.

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

Leave a comment

Comment anonymously or log in to comment.

Comments

Leave a reply

Reply anonymously or log in to reply.