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

Une autre implémentation de captcha pour Flask et WTForms

Brûlons leurs GPU à apprentissage profond.

4 juillet 2019
Dans Flask, WTForms
post main image
Original photo unsplash.com/@barnikakovacs.

Dans le passé, j'ai écrit un captcha en PHP pour limiter les inscriptions à la newsletter, ça fonctionnait bien, en fait il est toujours utilisé aujourd'hui. Vous ne pouvez pas vraiment bloquer les enregistrements de spam. Il y a des robots d'enregistrement mais il y a aussi des gens qui sont payés quelques dollars pour inonder votre site web avec de faux comptes ou des comptes trolls. C'est la réalité et nous devons l'affronter. Et maintenant il y a aussi l'apprentissage en profondeur qui peut être utilisé pour casser notre code captcha en seulement 15 minutes.

Nous, les personnes qui développent des sites Web, devons trouver des idées pour contester les faux enregistrements. Ce n'est pas possible mais peu de choses peuvent être faites et ajouter un captcha en fait partie.

Pour beaucoup de choses probablement ennuyeuses, vous pouvez trouver suffisamment de bibliothèques qui le font pour vous. Moi, je ne veux pas utiliser une bibliothèque, je veux écrire mon propre captcha en Python, apprendre bébé. Et parce que je respecte la vie privée des visiteurs de mes sites Web, je ne veux pas non plus utiliser ReCaptcha ou une autre solution servie à distance.

La solution de captcha présentée ci-dessous fonctionne mais n'est pas terminée, il suffit d'ajouter une distorsion de base. Ce n'est pas vraiment spécifique à Flask, c'est juste Python. J'ai utilisé BytesIO pour la première fois pour enregistrer une image non pas dans un fichier mais dans la mémoire. Ce qui est spécifique à Flask, c'est comment l'afficher dans un formulaire d'inscription et dans une page web. Notez qu'il ne s'agit pas d'une solution complète, mais simplement d'un détail des parties les plus importantes.

Voici comment le captcha est généré. captcha_fonts_path() est une fonction que vous devez créer pour importer la ou les polices pour le 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

Pour afficher l'image captcha avec Flask dans votre navigateur :

@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

Maintenant, nous devrions aussi montrer l'image captcha sous une forme. J'utilise Flask avec Flask-WTForms et j'ai une macro Jinja qui met tous les champs du formulaire sur la page. Cela fait gagner tellement de temps ! Pour mettre l'image captcha sous la forme que nous avons besoin d'un widget. Comment faire cela ? Je ne suis certainement pas un pro de WTForms et je crois qu'il devrait y avoir beaucoup plus d'exemples de widgets. Voici ma solution. Dans forms.py j'ai créé un widget pour CaptchaCodeField, il ne fait rien de plus que mettre l'image à l'écran :

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

Ensuite, dans views.py, je bourre l'url de l'image captcha dans ce champ avant d'appeler 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)

Je pense qu'il vaut encore la peine de mettre un captcha sur votre site Web si les visiteurs de votre site n'ont pas à payer pour quelque chose. Bien sûr, nous avons maintenant un apprentissage en profondeur, mais la boîte à outils des développeurs de captcha est aussi plus remplie aujourd'hui. Pensez à utiliser des polices différentes, des polices différentes en largeur et en hauteur, ajoutez de la distorsion, inversez les couleurs, soyez créatif, soyez imprévisible, gravez leurs GPU deeplearning ! Vous pouvez également envisager d'autres solutions de captcha comme les questions-réponses.

ImportError : Le module _imagingft C n'est pas installé

Bien sûr, un autre message d'erreur s'est affiché lors de l'utilisation de Pillow, voir ci-dessus. Ceci peut être résolu en ajoutant freetype, freetype-dev. Vous pouvez le vérifier en entrant votre conteneur et en devenant interactif en cours d'exécution :

phython3

puis en tapant à la machine :

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

Si freetype est installé, cela retournera Vrai, si non installé, cela retournera Faux. Comme j'utilise Docker et l'image Alpine, j'ai dû modifier mon 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

Liens / crédits

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

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.