From 12f3b044c5910978d0b60bdad3e2587cebe5a29c Mon Sep 17 00:00:00 2001 From: Tom Aisthorpe Date: Tue, 28 Apr 2026 18:17:20 +0100 Subject: [PATCH 1/2] Add support for Django ASGI --- aikido_zen/sources/django/__init__.py | 30 +++++++-- .../sources/django/pre_response_middleware.py | 13 +++- end2end/django_asgi_uvicorn_test.py | 61 +++++++++++++++++++ 3 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 end2end/django_asgi_uvicorn_test.py diff --git a/aikido_zen/sources/django/__init__.py b/aikido_zen/sources/django/__init__.py index 8b32611ee..5d8b66d12 100644 --- a/aikido_zen/sources/django/__init__.py +++ b/aikido_zen/sources/django/__init__.py @@ -2,9 +2,12 @@ from ..functions.request_handler import request_handler from .run_init_stage import run_init_stage -from .pre_response_middleware import pre_response_middleware +from .pre_response_middleware import ( + pre_response_middleware, + pre_response_middleware_async, +) from ...helpers.get_argument import get_argument -from ...sinks import on_import, patch_function, before, after +from ...sinks import on_import, patch_function, before, after, before_async, after_async @before @@ -25,13 +28,32 @@ def _get_response_after(func, instance, args, kwargs, return_value): request_handler(stage="post_response", status_code=return_value.status_code) +@before_async +async def _get_response_async_before(func, instance, args, kwargs): + request = get_argument(args, kwargs, 0, "request") + + run_init_stage(request) + + if pre_response_middleware_async not in getattr(instance, "_view_middleware"): + # pylint:disable=protected-access + instance._view_middleware += [pre_response_middleware_async] + + +@after_async +async def _get_response_async_after(func, instance, args, kwargs, return_value): + if hasattr(return_value, "status_code"): + request_handler(stage="post_response", status_code=return_value.status_code) + + @on_import("django.core.handlers.base", "django") def patch(m): """ - Patch for _get_response (Synchronous/WSGI) + Patch for _get_response (WSGI) and _get_response_async (ASGI) - before: Parse body, create context & add middleware to run before a response - - after: Check respone code to see if route should be analyzed + - after: Check response code to see if route should be analyzed # https://github.com/django/django/blob/5865ff5adcf64da03d306dc32b36e87ae6927c85/django/core/handlers/base.py#L174 """ patch_function(m, "BaseHandler._get_response", _get_response_before) patch_function(m, "BaseHandler._get_response", _get_response_after) + patch_function(m, "BaseHandler._get_response_async", _get_response_async_before) + patch_function(m, "BaseHandler._get_response_async", _get_response_async_after) diff --git a/aikido_zen/sources/django/pre_response_middleware.py b/aikido_zen/sources/django/pre_response_middleware.py index e90ebe14d..08dc597b1 100644 --- a/aikido_zen/sources/django/pre_response_middleware.py +++ b/aikido_zen/sources/django/pre_response_middleware.py @@ -1,4 +1,4 @@ -"""Exports pre_response_middleware function""" +"""Exports pre_response_middleware and pre_response_middleware_async functions""" from ..functions.request_handler import request_handler @@ -12,3 +12,14 @@ def pre_response_middleware(request, *args, **kwargs): return HttpResponse(response[0], status=response[1]) return None + + +async def pre_response_middleware_async(request, *args, **kwargs): + """Async variant of pre_response_middleware for Django ASGI""" + response = request_handler(stage="pre_response") + if response: + # pylint:disable=import-outside-toplevel # We don't want to install this by default + from django.http import HttpResponse + + return HttpResponse(response[0], status=response[1]) + return None diff --git a/end2end/django_asgi_uvicorn_test.py b/end2end/django_asgi_uvicorn_test.py new file mode 100644 index 000000000..db4eff0c4 --- /dev/null +++ b/end2end/django_asgi_uvicorn_test.py @@ -0,0 +1,61 @@ +import pytest +import requests +import time +from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type, \ + clear_events_from_mock + +# e2e tests for django-asgi-uvicorn sample app +post_url_fw = "http://localhost:8114/create" +post_url_nofw = "http://localhost:8115/create" + + +def test_firewall_started_okay(): + events = fetch_events_from_mock("http://localhost:5000") + started_events = filter_on_event_type(events, "started") + assert len(started_events) == 1 + validate_started_event(started_events[0], ["gunicorn", "django", "psycopg"]) + + +def test_safe_response_with_firewall(): + dog_name = "Bobby Tables" + res = requests.post(post_url_fw, data={"dog_name": dog_name}) + assert res.status_code == 200 + + +def test_safe_response_without_firewall(): + dog_name = "Bobby Tables" + res = requests.post(post_url_nofw, data={"dog_name": dog_name}) + assert res.status_code == 200 + + +def test_dangerous_response_with_firewall(): + clear_events_from_mock("http://localhost:5000") + dog_name = "Dangerous bobby', TRUE); -- " + res = requests.post(post_url_fw, data={"dog_name": dog_name}) + assert res.status_code == 500 + + time.sleep(5) # Wait for attack to be reported + events = fetch_events_from_mock("http://localhost:5000") + attacks = filter_on_event_type(events, "detected_attack") + + assert len(attacks) == 1 + del attacks[0]["attack"]["stack"] + assert attacks[0]["attack"] == { + "blocked": True, + "kind": "sql_injection", + "metadata": { + "dialect": "postgres", + "sql": "INSERT INTO sample_app_dogs (dog_name, is_admin) VALUES ('Dangerous bobby', TRUE); -- ', FALSE)", + }, + "operation": "psycopg.AsyncCursor.execute", + "pathToPayload": ".dog_name.[0]", + "payload": "\"Dangerous bobby', TRUE); -- \"", + "source": "body", + "user": None, + } + + +def test_dangerous_response_without_firewall(): + dog_name = "Dangerous bobby', TRUE); -- " + res = requests.post(post_url_nofw, data={"dog_name": dog_name}) + assert res.status_code == 200 From 493ee2a8a3a67853b157738f8391fb67c9d4ae4b Mon Sep 17 00:00:00 2001 From: Tom Aisthorpe Date: Wed, 29 Apr 2026 10:02:54 +0100 Subject: [PATCH 2/2] Add django asgi e2e test to CI --- .github/workflows/end2end.yml | 1 + README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/end2end.yml b/.github/workflows/end2end.yml index 16d79461b..541eec5a8 100644 --- a/.github/workflows/end2end.yml +++ b/.github/workflows/end2end.yml @@ -26,6 +26,7 @@ jobs: strategy: matrix: app: + - { name: django-asgi-uvicorn, testfile: end2end/django_asgi_uvicorn_test.py } - { name: django-mysql, testfile: end2end/django_mysql_test.py } - { name: django-mysql-gunicorn, testfile: end2end/django_mysql_gunicorn_test.py } - { name: django-postgres-gunicorn, testfile: end2end/django_postgres_gunicorn_test.py } diff --git a/README.md b/README.md index a58d655d5..73be0762a 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Zen for Python 3 is compatible with: * ✅ [uWSGI](docs/uwsgi.md) ### ASGI +* ✅ [Django](docs/django.md) * ✅ [Quart](docs/quart.md) * ✅ [Starlette](docs/starlette.md) ^0.16 * ✅ [FastAPI](docs/fastapi.md) ^0.70