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

Een andere captcha implementatie voor Flask en WTForms

Laten we hun dieplerende GPU's verbranden.

4 juli 2019
post main image
Original photo unsplash.com/@barnikakovacs.

In het verleden schreef ik een captcha in PHP te beperken e-mail nieuwsbrief aanmeldingen, werkte prima, in feite is het nog steeds in gebruik vandaag de dag. U kunt niet echt spam registraties blokkeren. Er zijn registratierobots, maar er zijn ook mensen die een paar dollar betaald krijgen om uw website te overspoelen met valse of trollenaccounts. Dat is de realiteit en dat moeten we onder ogen zien. En nu is er ook deeplearning die gebruikt kan worden om onze captcha code in slechts 15 minuten te breken.

Wij, de ontwikkelaars van websites, moeten met ideeën komen om namaakregistraties aan te vechten. Het is niet mogelijk, maar er kunnen weinig dingen worden gedaan en het toevoegen van een captcha is er een van.

Voor veel waarschijnlijk saaie dingen, kun je genoeg bibliotheken vinden die dit voor je doen. Ik, ik wil geen gebruik maken van een bibliotheek, ik wil mijn eigen captcha in schrijven Python, baby leren. En omdat ik de privacy van de bezoekers van mijn websites op prijs stel, wil ik ook geen gebruik maken van ReCaptcha of een andere op afstand bediende oplossing.

De onderstaande captcha oplossing werkt, maar is nog niet af, er moet alleen een basisvervorming worden toegevoegd. Het is niet echt Flask specifiek, het is gewoon Python. Ik heb BytesIO voor het eerst gebruikt om een afbeelding niet in een bestand maar in het geheugen op te slaan. Het Flask specifieke is hoe het in een aanmeldingsformulier en op een webpagina wordt weergegeven. Merk op dat dit geen complete oplossing is, maar alleen de belangrijkste onderdelen.

Hier is hoe het captcha wordt gegenereerd. captcha_fonts_path() is een functie die u moet maken om de font(en) voor de 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

Om de captcha afbeelding te tonen met Flask in uw 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

Nu moeten we het captcha beeld ook in een vorm laten zien. Ik gebruik Flask met Flask-WTForms en heb een Jinja macro die alle formuliervelden op de pagina zet. Dit bespaart zoveel tijd! Om de captcha afbeelding in de vorm van een widget te zetten hebben we een widget nodig. Hoe doe je dit? Ik ben zeker geen WTForms pro en ik ben van mening dat er veel meer voorbeelden van widgets moeten komen. Hier is mijn oplossing. In forms.py heb ik een widget gemaakt voor CaptchaCodeField, het doet niets anders dan de afbeelding op het scherm zetten:

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())

Dan in views.py stop ik het captcha beeld url in dit veld voordat ik bel 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)

Ik denk dat het nog steeds de moeite waard is om een captcha op uw website te plaatsen als uw websitebezoekers niet voor iets hoeven te betalen. Natuurlijk hebben we nu deeplearning, maar de toolbox van de captcha ontwikkelaars is ook meer gevuld vandaag de dag. Denk aan het gebruik van verschillende lettertypen, verschillende breedte- en hoogtelettertypen, voeg vervorming toe, omgekeerde kleuren, wees creatief, wees onvoorspelbaar, laten we hun deeplearning GPU's verbranden! Misschien wilt u ook andere captcha oplossingen overwegen, zoals vragen en antwoorden.

Importfout: De _imagingft C-module is niet geïnstalleerd.

Natuurlijk kwam er nog een mooie foutmelding bij het gebruik van Pillow, zie hierboven. Dit kan worden opgelost door freetype, freetype-dev toe te voegen. U kunt dit controleren door uw container in te gaan en interactief te gaan lopen:

phython3

en dan typen:

from PIL import features
features.check('freetype2')

Als freetype is geïnstalleerd zal dit terugkeren naar True, als het niet is geïnstalleerd zal het False terugkeren. Omdat ik gebruik maak van Docker en de Alpine afbeelding die ik heb moeten wijzigen in mijn Dockerbestand:

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

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.