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

Otra implementación de captcha para Flask y WTForms

Quememos sus GPUs de aprendizaje profundo.

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

En el pasado escribí un captcha en PHP para limitar el registro de boletines de noticias por correo electrónico, funcionó bien, de hecho todavía está en uso hoy en día. No se pueden bloquear los registros de spam. Hay robots de registro, pero también hay personas a las que se les paga unos cuantos dólares para que inunden su sitio web con cuentas falsas o troll. Esa es la realidad y tenemos que afrontarla. Y ahora también hay un aprendizaje profundo que puede ser usado para romper nuestro código de captcha en sólo 15 minutos.

Nosotros, las personas que desarrollamos sitios web, debemos tener ideas para desafiar los registros falsos. No es posible, pero se pueden hacer pocas cosas y añadir un captcha es una de ellas.

Para muchas cosas probablemente aburridas, puedes encontrar suficientes bibliotecas haciendo esto por ti. Yo, no quiero usar una biblioteca, quiero escribir mi propio captcha en Python, aprender bebé. Y debido a que valoro la privacidad de los visitantes de mis sitios web, tampoco quiero usar ReCaptcha u otra solución servida remotamente.

La solución de captcha que se presenta a continuación funciona pero no está terminada, sólo hay que añadir la distorsión básica. No es realmente específico de Flask, es sólo Python. Utilicé BytesIO por primera vez para guardar una imagen no en un archivo sino en la memoria. Lo que es específico de Flask es cómo mostrarlo en un formulario de registro y en una página web. Tenga en cuenta que esta no es una solución completa, sólo detalla las partes más importantes.

Así es como se genera el captcha. captcha_fonts_path() es una función que necesita hacer para importar la(s) fuente(s) para el 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

Para mostrar la imagen captcha con Flask en su navegador:

@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

Ahora también deberíamos mostrar la imagen captcha en un formulario. Utilizo Flask con Flask-WTForms y tengo una macro Jinja que pone todos los campos del formulario en la página. Esto ahorra mucho tiempo! Para poner la imagen captcha en la forma necesitamos un widget. ¿Cómo hacer esto? Ciertamente no soy un profesional de WTForms y creo que debería haber muchos más ejemplos de widgets. Esta es mi solución. En form.py he creado un widget para CaptchaCodeField, que no hace más que poner la imagen en la pantalla:

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

Luego en views.py relleno la url de la imagen captcha en este campo antes de llamar a 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)

Creo que todavía vale la pena poner un captcha en su sitio web si los visitantes de su sitio web no tienen que pagar por algo. Por supuesto que ahora tenemos un aprendizaje profundo, pero la caja de herramientas de desarrolladores de captcha también está más llena hoy en día. Piensa en usar diferentes tipos de letra, diferentes tipos de letra, diferentes anchos y alturas, añade distorsión, invierte los colores, sé creativo, sé impredecible, ¡quememos sus GPUs de aprendizaje profundo! También puede considerar otras soluciones de captcha como Q&A.

ImportarError: El módulo _imagingft C no está instalado

Por supuesto, otro mensaje de error agradable apareció al usar Pillow, ver arriba. Esto se puede resolver añadiendo freetype, freetype-dev. Puede comprobar esto entrando en su contenedor e interactuando ejecutando:

phython3

y luego escribir a máquina:

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

Si freetype está instalado esto devolverá True, si no está instalado devolverá False. Como estoy usando Docker y la imagen Alpine, tuve que hacer cambios en mi 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

Enlaces / créditos

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

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.