From 72cf5a3d5e5deeee5c2f08c1d0b9ac3f0d5ec585 Mon Sep 17 00:00:00 2001 From: Sli Date: Wed, 10 Jul 2024 12:24:41 +0200 Subject: [PATCH] Introduce honeypot for login/registering/password changing --- core/templates/core/login.jinja | 1 + core/templates/core/password_reset.jinja | 1 + core/templates/core/register.jinja | 1 + core/templatetags/extensions.py | 58 ++++++++++++++++++++++++ core/tests.py | 32 ++++++++++++- core/views/user.py | 9 +++- poetry.lock | 36 ++++++++++----- pyproject.toml | 1 + sith/settings.py | 10 +++- 9 files changed, 132 insertions(+), 17 deletions(-) create mode 100644 core/templatetags/extensions.py diff --git a/core/templates/core/login.jinja b/core/templates/core/login.jinja index b76bc46c..4e56613d 100644 --- a/core/templates/core/login.jinja +++ b/core/templates/core/login.jinja @@ -33,6 +33,7 @@ {% endif %} {% csrf_token %} + {% render_honeypot_field %}
diff --git a/core/templates/core/password_reset.jinja b/core/templates/core/password_reset.jinja index f54a255d..b0a63fc3 100644 --- a/core/templates/core/password_reset.jinja +++ b/core/templates/core/password_reset.jinja @@ -3,6 +3,7 @@ {% block content %}
{% csrf_token %} +{% render_honeypot_field %} {{ form.as_p() }}
diff --git a/core/templates/core/register.jinja b/core/templates/core/register.jinja index 681d2d48..4702f49c 100644 --- a/core/templates/core/register.jinja +++ b/core/templates/core/register.jinja @@ -23,6 +23,7 @@ {% else %}
{% csrf_token %} + {% render_honeypot_field %} {{ form }}
diff --git a/core/templatetags/extensions.py b/core/templatetags/extensions.py new file mode 100644 index 00000000..6c2aa4ca --- /dev/null +++ b/core/templatetags/extensions.py @@ -0,0 +1,58 @@ +# +# Copyright 2024 +# - Sli +# +# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, +# http://ae.utbm.fr. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License a published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., 59 Temple +# Place - Suite 330, Boston, MA 02111-1307, USA. +# +# +from typing import Callable + +import honeypot.templatetags.honeypot as honeypot_filters +from django.template.loader import render_to_string +from jinja2 import Environment, nodes +from jinja2.ext import Extension +from jinja2.parser import Parser + + +class HoneypotExtension(Extension): + """ + Wrapper around the honeypot extension tag + Known limitation: doesn't support arguments + + Usage: {% render_honeypot_field %} + """ + + tags = {"render_honeypot_field"} + + def __init__(self, environment: Environment) -> None: + environment.globals["render_honeypot_field"] = ( + honeypot_filters.render_honeypot_field + ) + self.environment = environment + + def parse(self, parser: Parser) -> nodes.Output: + lineno = parser.stream.expect("name:render_honeypot_field").lineno + call = self.call_method( + "_render", + [nodes.Name("render_honeypot_field", "load", lineno=lineno)], + lineno=lineno, + ) + return nodes.Output([nodes.MarkSafe(call)]) + + def _render(self, render_honeypot_field: Callable[[str | None], str]): + return render_to_string("honeypot/honeypot_field.html", render_honeypot_field()) diff --git a/core/tests.py b/core/tests.py index b476c588..f4163388 100644 --- a/core/tests.py +++ b/core/tests.py @@ -36,6 +36,7 @@ class TestUserRegistration: @pytest.fixture() def valid_payload(self): return { + settings.HONEYPOT_FIELD_NAME: settings.HONEYPOT_VALUE, "first_name": "this user does not exist (yet)", "last_name": "this user does not exist (yet)", "email": "i-dont-exist-yet@git.an", @@ -68,6 +69,13 @@ class TestUserRegistration: assert response.status_code == 200 assert "TEST_REGISTER_USER_FORM_FAIL" in str(response.content) + def test_register_honeypot_fail(self, client, valid_payload): + payload = valid_payload | { + settings.HONEYPOT_FIELD_NAME: settings.HONEYPOT_VALUE + "random" + } + response = client.post(reverse("core:register"), payload) + assert response.status_code == 400 + def test_register_user_form_fail_already_exists(self, client, valid_payload): """Should not register a user correctly if it already exists.""" # create the user, then try to create it again @@ -90,7 +98,11 @@ class TestUserLogin: response = client.post( reverse("core:login"), - {"username": user.username, "password": "wrong-password"}, + { + "username": user.username, + "password": "wrong-password", + settings.HONEYPOT_FIELD_NAME: settings.HONEYPOT_VALUE, + }, ) assert response.status_code == 200 assert ( @@ -98,12 +110,28 @@ class TestUserLogin: "et votre mot de passe ne correspondent pas. Merci de réessayer.

" ) in str(response.content.decode()) + def test_login_honeypot(self, client, user): + response = client.post( + reverse("core:login"), + { + "username": user.username, + "password": "wrong-password", + settings.HONEYPOT_FIELD_NAME: settings.HONEYPOT_VALUE + "incorrect", + }, + ) + assert response.status_code == 400 + def test_login_success(self, client, user): """ Should login a user correctly """ response = client.post( - reverse("core:login"), {"username": user.username, "password": "plop"} + reverse("core:login"), + { + "username": user.username, + "password": "plop", + settings.HONEYPOT_FIELD_NAME: settings.HONEYPOT_VALUE, + }, ) assertRedirects(response, reverse("core:index")) diff --git a/core/views/user.py b/core/views/user.py index 4c91b8ff..6952a8b1 100644 --- a/core/views/user.py +++ b/core/views/user.py @@ -36,6 +36,7 @@ from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.template.response import TemplateResponse from django.urls import reverse, reverse_lazy +from django.utils.decorators import method_decorator from django.utils.translation import gettext as _ from django.views.generic import ( CreateView, @@ -46,6 +47,7 @@ from django.views.generic import ( ) from django.views.generic.dates import MonthMixin, YearMixin from django.views.generic.edit import UpdateView +from honeypot.decorators import check_honeypot from api.views.sas import all_pictures_of_user from core.models import Gift, Preferences, SithFile, User @@ -69,6 +71,7 @@ from subscription.models import Subscription from trombi.views import UserTrombiForm +@method_decorator(check_honeypot, name="post") class SithLoginView(views.LoginView): """ The login View @@ -124,9 +127,10 @@ def password_root_change(request, user_id): ) +@method_decorator(check_honeypot, name="post") class SithPasswordResetView(views.PasswordResetView): """ - Allows someone to enter an email adresse for resetting password + Allows someone to enter an email address for resetting password """ template_name = "core/password_reset.jinja" @@ -153,12 +157,13 @@ class SithPasswordResetConfirmView(views.PasswordResetConfirmView): class SithPasswordResetCompleteView(views.PasswordResetCompleteView): """ - Confirm the password has sucessfully been reset + Confirm the password has successfully been reset """ template_name = "core/password_reset_complete.jinja" +@check_honeypot def register(request): context = {} if request.method == "POST": diff --git a/poetry.lock b/poetry.lock index 0cb95720..2c875fcd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -426,13 +426,13 @@ files = [ [[package]] name = "django" -version = "4.2.13" +version = "4.2.14" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.8" files = [ - {file = "Django-4.2.13-py3-none-any.whl", hash = "sha256:a17fcba2aad3fc7d46fdb23215095dbbd64e6174bf4589171e732b18b07e426a"}, - {file = "Django-4.2.13.tar.gz", hash = "sha256:837e3cf1f6c31347a1396a3f6b65688f2b4bb4a11c580dcb628b5afe527b68a5"}, + {file = "Django-4.2.14-py3-none-any.whl", hash = "sha256:3ec32bc2c616ab02834b9cac93143a7dc1cdcd5b822d78ac95fc20a38c534240"}, + {file = "Django-4.2.14.tar.gz", hash = "sha256:fc6919875a6226c7ffcae1a7d51e0f2ceaf6f160393180818f6c95f51b1e7b96"}, ] [package.dependencies] @@ -477,17 +477,17 @@ test = ["djangorestframework", "graphene-django", "pytest", "pytest-cov", "pytes [[package]] name = "django-debug-toolbar" -version = "4.3.0" +version = "4.4.5" description = "A configurable set of panels that display various debug information about the current request/response." optional = false python-versions = ">=3.8" files = [ - {file = "django_debug_toolbar-4.3.0-py3-none-any.whl", hash = "sha256:e09b7dcb8417b743234dfc57c95a7c1d1d87a88844abd13b4c5387f807b31bf6"}, - {file = "django_debug_toolbar-4.3.0.tar.gz", hash = "sha256:0b0dddee5ea29b9cb678593bc0d7a6d76b21d7799cb68e091a2148341a80f3c4"}, + {file = "django_debug_toolbar-4.4.5-py3-none-any.whl", hash = "sha256:91425606673ee674d780f7aeedf3595c264eb382dcf41f55c6779577900904c0"}, + {file = "django_debug_toolbar-4.4.5.tar.gz", hash = "sha256:8298ce966b4c8fc71430082dd4739ef2badb5f867734e1973a413c4ab2ea81b7"}, ] [package.dependencies] -django = ">=3.2.4" +django = ">=4.2.9" sqlparse = ">=0.2" [[package]] @@ -508,6 +508,20 @@ packaging = "*" elasticsearch = ["elasticsearch (>=5,<8)"] testing = ["coverage", "geopy (==2)", "pysolr (>=3.7)", "python-dateutil", "requests", "whoosh (>=2.5.4,<3.0)"] +[[package]] +name = "django-honeypot" +version = "1.2.0" +description = "Django honeypot field utilities" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "django_honeypot-1.2.0-py3-none-any.whl", hash = "sha256:53dd5f8dd96ef1bb7e31b5514c0dc2caae9577e78ebdf03ca4e0f304a7422aba"}, + {file = "django_honeypot-1.2.0.tar.gz", hash = "sha256:25fca02e786aec26649bd13b37a95c846e09ab3cfc10f28db2f7dfaa77b9b9c6"}, +] + +[package.dependencies] +Django = ">=3.2,<5.1" + [[package]] name = "django-jinja" version = "2.11.0" @@ -669,13 +683,13 @@ python-dateutil = ">=2.7" [[package]] name = "identify" -version = "2.5.36" +version = "2.6.0" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, - {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, + {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, + {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, ] [package.extras] @@ -1867,4 +1881,4 @@ filelock = ">=3.4" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "8524ed5f593973edf05b3c01010c1f2345b7e799089c3e38274304bdedf8b3cb" +content-hash = "51820883f41bdf40f00296b722ebdd9ac386e43ef1424ef990b29bac579ecbab" diff --git a/pyproject.toml b/pyproject.toml index a52ffc52..abd42aca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ django-countries = "^7.5.1" dict2xml = "^1.7.3" Sphinx = "^5" # Needed for building xapian tomli = "^2.0.1" +django-honeypot = "^1.2.0" [tool.poetry.group.dev.dependencies] django-debug-toolbar = "^4.0.0" diff --git a/sith/settings.py b/sith/settings.py index 42d7b8d9..ab311a85 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -1,5 +1,5 @@ # -# Copyright 2016,2017 +# Copyright 2016,2017,2024 # - Skia # - Sli # @@ -17,7 +17,7 @@ # details. # # You should have received a copy of the GNU General Public License along with -# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# this program; if not, write to the Free Software Foundation, Inc., 59 Temple # Place - Suite 330, Boston, MA 02111-1307, USA. # # @@ -54,6 +54,10 @@ os.environ["HTTPS"] = "off" # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "(4sjxvhz@m5$0a$j0_pqicnc$s!vbve)z+&++m%g%bjhlz4+g2" +# Those values are to be changed in production to be more effective +HONEYPOT_FIELD_NAME = "body2" +HONEYPOT_VALUE = "content" + # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False TESTING = "pytest" in sys.modules @@ -75,6 +79,7 @@ INSTALLED_APPS = ( "django.contrib.messages", "django.contrib.staticfiles", "django.contrib.sites", + "honeypot", "django_jinja", "rest_framework", "ajax_select", @@ -143,6 +148,7 @@ TEMPLATES = [ "django_jinja.builtins.extensions.UrlsExtension", "django_jinja.builtins.extensions.StaticFilesExtension", "django_jinja.builtins.extensions.DjangoFiltersExtension", + "core.templatetags.extensions.HoneypotExtension", ], "filters": { "markdown": "core.templatetags.renderer.markdown",