Merge pull request #726 from ae-utbm/honeypot

better honeypot logging
This commit is contained in:
thomas girod 2024-07-22 12:45:45 +02:00 committed by GitHub
commit 811e5a5ad1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 70 additions and 9 deletions

View File

@ -47,12 +47,23 @@ class HoneypotExtension(Extension):
def parse(self, parser: Parser) -> nodes.Output: def parse(self, parser: Parser) -> nodes.Output:
lineno = parser.stream.expect("name:render_honeypot_field").lineno lineno = parser.stream.expect("name:render_honeypot_field").lineno
key = nodes.Name("render_honeypot_field", "load", lineno=lineno)
if parser.stream.current.type != "block_end":
field_name = parser.parse_expression()
else:
field_name = nodes.Const(None)
call = self.call_method( call = self.call_method(
"_render", "_render",
[nodes.Name("render_honeypot_field", "load", lineno=lineno)], [key, field_name],
lineno=lineno, lineno=lineno,
) )
return nodes.Output([nodes.MarkSafe(call)]) return nodes.Output([nodes.MarkSafe(call)])
def _render(self, render_honeypot_field: Callable[[str | None], str]): def _render(
return render_to_string("honeypot/honeypot_field.html", render_honeypot_field()) self,
render_honeypot_field: Callable[[str | None], str],
field_name: str | None = None,
):
return render_to_string(
"honeypot/honeypot_field.html", render_honeypot_field(field_name=field_name)
)

View File

@ -25,6 +25,7 @@ from typing import Optional
import PIL import PIL
from django.conf import settings from django.conf import settings
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.http import HttpRequest
from django.utils import timezone from django.utils import timezone
from PIL import ExifTags from PIL import ExifTags
from PIL.Image import Resampling from PIL.Image import Resampling
@ -297,3 +298,16 @@ def bbcode_to_markdown(text):
new_text.append(line) new_text.append(line)
return "\n".join(new_text) return "\n".join(new_text)
def get_client_ip(request: HttpRequest) -> str | None:
headers = (
"X_FORWARDED_FOR", # Common header for proixes
"FORWARDED", # Standard header defined by RFC 7239.
"REMOTE_ADDR", # Default IP Address (direct connection)
)
for header in headers:
if (ip := request.META.get(header)) is not None:
return ip
return None

View File

@ -29,6 +29,7 @@
{% endif %} {% endif %}
<form action="" method="post" enctype="multipart/form-data"> <form action="" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{% render_honeypot_field settings.HONEYPOT_FIELD_NAME_FORUM %}
{{ form.as_p() }} {{ form.as_p() }}
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p> <p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
</form> </form>

View File

@ -14,6 +14,7 @@
# #
import pytest import pytest
from django.conf import settings
from django.test import Client from django.test import Client
from django.urls import reverse from django.urls import reverse
from pytest_django.asserts import assertRedirects from pytest_django.asserts import assertRedirects
@ -24,13 +25,14 @@ from forum.models import Forum, ForumMessage, ForumTopic
@pytest.mark.django_db @pytest.mark.django_db
class TestTopicCreation: class TestTopicCreation:
def test_topic_creation_success(self, client: Client): def test_topic_creation_ok(self, client: Client):
user: User = User.objects.get(username="root") user: User = User.objects.get(username="root")
forum = Forum.objects.get(name="AE") forum = Forum.objects.get(name="AE")
client.force_login(user) client.force_login(user)
payload = { payload = {
"title": "Hello IT.", "title": "Hello IT.",
"message": "Have you tried turning it off and on again ?", "message": "Have you tried turning it off and on again ?",
settings.HONEYPOT_FIELD_NAME_FORUM: settings.HONEYPOT_VALUE,
} }
assert not ForumTopic.objects.filter(_title=payload["title"]).exists() assert not ForumTopic.objects.filter(_title=payload["title"]).exists()
response = client.post(reverse("forum:new_topic", args=str(forum.id)), payload) response = client.post(reverse("forum:new_topic", args=str(forum.id)), payload)
@ -46,13 +48,28 @@ class TestTopicCreation:
assert topic assert topic
assert topic.last_message.message == payload["message"] assert topic.last_message.message == payload["message"]
def test_topic_creation_failure(self, client: Client): def test_topic_creation_honeypot_fail(self, client: Client):
user: User = User.objects.get(username="root")
forum = Forum.objects.get(name="AE")
client.force_login(user)
payload = {
"title": "You shall",
"message": "Not pass !",
settings.HONEYPOT_FIELD_NAME_FORUM: settings.HONEYPOT_VALUE + "random",
}
assert not ForumTopic.objects.filter(_title=payload["title"]).exists()
response = client.post(reverse("forum:new_topic", args=str(forum.id)), payload)
assert response.status_code == 200
assert not ForumTopic.objects.filter(_title=payload["title"]).exists()
def test_topic_creation_fail(self, client: Client):
user: User = User.objects.get(username="krophil") user: User = User.objects.get(username="krophil")
forum = Forum.objects.get(name="AE") forum = Forum.objects.get(name="AE")
client.force_login(user) client.force_login(user)
payload = { payload = {
"title": "You shall", "title": "You shall",
"message": "Not pass !", "message": "Not pass !",
settings.HONEYPOT_FIELD_NAME_FORUM: settings.HONEYPOT_VALUE,
} }
assert not ForumTopic.objects.filter(_title=payload["title"]).exists() assert not ForumTopic.objects.filter(_title=payload["title"]).exists()
response = client.post(reverse("forum:new_topic", args=str(forum.id)), payload) response = client.post(reverse("forum:new_topic", args=str(forum.id)), payload)

View File

@ -22,6 +22,7 @@
# #
# #
import math import math
from functools import partial
from ajax_select import make_ajax_field from ajax_select import make_ajax_field
from django import forms from django import forms
@ -32,11 +33,13 @@ from django.db import IntegrityError
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import html, timezone from django.utils import html, timezone
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView, RedirectView from django.views.generic import DetailView, ListView, RedirectView
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import CreateView, DeleteView, UpdateView from django.views.generic.edit import CreateView, DeleteView, UpdateView
from haystack.query import RelatedSearchQuerySet from haystack.query import RelatedSearchQuerySet
from honeypot.decorators import check_honeypot
from core.views import ( from core.views import (
CanCreateMixin, CanCreateMixin,
@ -242,6 +245,9 @@ class TopicForm(forms.ModelForm):
title = forms.CharField(required=True, label=_("Title")) title = forms.CharField(required=True, label=_("Title"))
@method_decorator(
partial(check_honeypot, field_name=settings.HONEYPOT_FIELD_NAME_FORUM), name="post"
)
class ForumTopicCreateView(CanCreateMixin, CreateView): class ForumTopicCreateView(CanCreateMixin, CreateView):
model = ForumMessage model = ForumMessage
form_class = TopicForm form_class = TopicForm
@ -331,6 +337,9 @@ class ForumMessageView(SingleObjectMixin, RedirectView):
return self.object.get_url() return self.object.get_url()
@method_decorator(
partial(check_honeypot, field_name=settings.HONEYPOT_FIELD_NAME_FORUM), name="post"
)
class ForumMessageEditView(CanEditMixin, UpdateView): class ForumMessageEditView(CanEditMixin, UpdateView):
model = ForumMessage model = ForumMessage
form_class = forms.modelform_factory( form_class = forms.modelform_factory(
@ -381,6 +390,9 @@ class ForumMessageUndeleteView(SingleObjectMixin, RedirectView):
return self.object.get_absolute_url() return self.object.get_absolute_url()
@method_decorator(
partial(check_honeypot, field_name=settings.HONEYPOT_FIELD_NAME_FORUM), name="post"
)
class ForumMessageCreateView(CanCreateMixin, CreateView): class ForumMessageCreateView(CanCreateMixin, CreateView):
model = ForumMessage model = ForumMessage
form_class = forms.modelform_factory( form_class = forms.modelform_factory(

View File

@ -1,12 +1,17 @@
import logging import logging
from time import localtime, strftime
from typing import Any from typing import Any
from django.http import HttpResponse from django.http import HttpRequest, HttpResponse
from django.test.client import WSGIRequest
from core.utils import get_client_ip
def custom_honeypot_error( def custom_honeypot_error(
request: WSGIRequest, context: dict[str, Any] request: HttpRequest, context: dict[str, Any]
) -> HttpResponse: ) -> HttpResponse:
logging.warning(f"HoneyPot blocked user with ip {request.META.get('REMOTE_ADDR')}") logging.warning(
f"[{strftime('%c', localtime())}] "
f"HoneyPot blocked user with ip {get_client_ip(request)}"
)
return HttpResponse("Upon reading this, the http client was enlightened.") return HttpResponse("Upon reading this, the http client was enlightened.")

View File

@ -287,6 +287,7 @@ REST_FRAMEWORK["UNAUTHENTICATED_USER"] = "core.models.AnonymousUser"
HONEYPOT_FIELD_NAME = "body2" HONEYPOT_FIELD_NAME = "body2"
HONEYPOT_VALUE = "content" HONEYPOT_VALUE = "content"
HONEYPOT_RESPONDER = custom_honeypot_error # Make honeypot errors less suspicious HONEYPOT_RESPONDER = custom_honeypot_error # Make honeypot errors less suspicious
HONEYPOT_FIELD_NAME_FORUM = "message2" # Only used on forum
# Email # Email