From f2083aeb251d9ecef2ee7927ce20344521ec4c77 Mon Sep 17 00:00:00 2001 From: nadiraikido <166383531+nadiraikido@users.noreply.github.com> Date: Mon, 7 Apr 2025 19:53:00 +0200 Subject: [PATCH 01/18] Prioritize JSON parsing for body --- aikido_zen/sources/django/run_init_stage.py | 13 ++- .../sources/django/run_init_stage_test.py | 30 +++++ end2end/django_mysql_test.py | 110 +++++++++++++----- 3 files changed, 115 insertions(+), 38 deletions(-) diff --git a/aikido_zen/sources/django/run_init_stage.py b/aikido_zen/sources/django/run_init_stage.py index 4b865c1ae..15493a905 100644 --- a/aikido_zen/sources/django/run_init_stage.py +++ b/aikido_zen/sources/django/run_init_stage.py @@ -10,6 +10,13 @@ def run_init_stage(request): """Parse request and body, run "init" stage with request_handler""" body = None try: + # Check for JSON + if body is None and request.content_type == "application/json": + try: + body = json.loads(request.body) + except Exception: + pass + # try-catch loading of form parameters, this is to fix issue with DATA_UPLOAD_MAX_NUMBER_FIELDS : try: body = request.POST.dict() @@ -18,12 +25,6 @@ def run_init_stage(request): except Exception: pass - # Check for JSON or XML : - if body is None and request.content_type == "application/json": - try: - body = json.loads(request.body) - except Exception: - pass if body is None or len(body) == 0: # E.g. XML Data body = request.body diff --git a/aikido_zen/sources/django/run_init_stage_test.py b/aikido_zen/sources/django/run_init_stage_test.py index 7afbf5275..497bdd0da 100644 --- a/aikido_zen/sources/django/run_init_stage_test.py +++ b/aikido_zen/sources/django/run_init_stage_test.py @@ -120,6 +120,36 @@ def test_run_init_stage_with_empty_body_string(mock_request): assert context.body is None +def test_run_init_stage_with_json_wrong_content_type(mock_request): + """Test run_init_stage with an XML request.""" + mock_request.content_type = "application/x-www-form-urlencoded" + mock_request.body = '{"key": "value"}' # Example XML body + run_init_stage(mock_request) + # Assertions + context: Context = get_current_context() + assert context.body == {"key": "value"} + + +def test_run_init_stage_with_xml_wrong_content_type(mock_request): + """Test run_init_stage with an XML request.""" + mock_request.content_type = "application/json" + mock_request.body = "value" # Example XML body + run_init_stage(mock_request) + # Assertions + context: Context = get_current_context() + assert context.body == "value" + + +def test_run_init_stage_with_xml_wrong_content_type_form_urlencoded(mock_request): + """Test run_init_stage with an XML request.""" + mock_request.content_type = "application/x-www-form-urlencoded" + mock_request.body = "value=2" # Example XML body + run_init_stage(mock_request) + # Assertions + context: Context = get_current_context() + assert context.body == "value=2" + + def test_run_init_stage_with_xml(mock_request): """Test run_init_stage with an XML request.""" mock_request.content_type = "application/xml" diff --git a/end2end/django_mysql_test.py b/end2end/django_mysql_test.py index 7e499be4d..6f5db62c5 100644 --- a/end2end/django_mysql_test.py +++ b/end2end/django_mysql_test.py @@ -1,89 +1,135 @@ import time import pytest import requests -from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type, validate_heartbeat +from .server.check_events_from_mock import ( + fetch_events_from_mock, + validate_started_event, + filter_on_event_type, + validate_heartbeat, +) # e2e tests for django_mysql sample app base_url_fw = "http://localhost:8080/app" base_url_nofw = "http://localhost:8081/app" + 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], ["django", "mysqlclient"]) + def test_safe_response_with_firewall(): dog_name = "Bobby Tables" - res = requests.post(base_url_fw + "/create", data={'dog_name': dog_name}) + res = requests.post(base_url_fw + "/create", data={"dog_name": dog_name}) assert res.status_code == 200 + def test_safe_response_without_firewall(): dog_name = "Bobby Tables" - res = requests.post(base_url_nofw + "/create", data={'dog_name': dog_name}) + res = requests.post(base_url_nofw + "/create", data={"dog_name": dog_name}) assert res.status_code == 200 def test_dangerous_response_with_firewall(): dog_name = 'Dangerous bobby", 1); -- ' - res = requests.post(base_url_fw + "/create", data={'dog_name': dog_name}) + res = requests.post(base_url_fw + "/create", 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": { + "sql": 'INSERT INTO sample_app_dogs (dog_name, dog_boss) VALUES ("Dangerous bobby", 1); -- ", "N/A")' + }, + "operation": "MySQLdb.Cursor.execute", + "pathToPayload": ".dog_name", + "payload": '"Dangerous bobby\\", 1); -- "', + "source": "body", + "user": None, + } + + +def test_dangerous_response_with_form_header_but_json_body(): + import json + + headers = {"Content-Type": "application/x-www-form-urlencoded"} + + json_body = json.dumps({"dog_name": 'Dangerous bobby", 1); -- '}) + + res = requests.post(base_url_fw + "/create", headers=headers, data=json_body) assert res.status_code == 500 - time.sleep(5) # Wait for attack to be reported + time.sleep(5) + 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': {'sql': 'INSERT INTO sample_app_dogs (dog_name, dog_boss) VALUES ("Dangerous bobby", 1); -- ", "N/A")'}, - 'operation': 'MySQLdb.Cursor.execute', - 'pathToPayload': '.dog_name', - 'payload': '"Dangerous bobby\\", 1); -- "', - 'source': "body", - 'user': None + "metadata": { + "sql": 'INSERT INTO sample_app_dogs (dog_name, dog_boss) VALUES ("Dangerous bobby", 1); -- ", "N/A")' + }, + "operation": "MySQLdb.Cursor.execute", + "pathToPayload": ".dog_name", + "payload": '"Dangerous bobby\\", 1); -- "', + "source": "body", + "user": None, } + def test_dangerous_response_with_firewall_shell(): dog_name = 'Dangerous bobby", 1); -- ' res = requests.get(base_url_fw + "/shell/ls -la") assert res.status_code == 500 - time.sleep(5) # Wait for attack to be reported + 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) == 2 - del attacks[0] # Previous attack + del attacks[0] # Previous attack del attacks[0]["attack"]["stack"] assert attacks[0]["attack"] == { "blocked": True, "kind": "shell_injection", - 'metadata': {'command': 'ls -la'}, - 'operation': 'subprocess.Popen', - 'pathToPayload': '.[0]', - 'payload': '"ls -la"', - 'source': "route_params", - 'user': None + "metadata": {"command": "ls -la"}, + "operation": "subprocess.Popen", + "pathToPayload": ".[0]", + "payload": '"ls -la"', + "source": "route_params", + "user": None, } + def test_dangerous_response_without_firewall(): dog_name = 'Dangerous bobby", 1); -- ' - res = requests.post(base_url_nofw + "/create", data={'dog_name': dog_name}) + res = requests.post(base_url_nofw + "/create", data={"dog_name": dog_name}) assert res.status_code == 200 + def test_initial_heartbeat(): - time.sleep(55) # Sleep 5 + 55 seconds for heartbeat + time.sleep(55) # Sleep 5 + 55 seconds for heartbeat events = fetch_events_from_mock("http://localhost:5000") heartbeat_events = filter_on_event_type(events, "heartbeat") assert len(heartbeat_events) == 1 - validate_heartbeat(heartbeat_events[0], - [{ - "apispec": {}, - "hits": 1, - "hits_delta_since_sync": 1, - "method": "POST", - "path": "/app/create" - }], - {"aborted":0,"attacksDetected":{"blocked":2,"total":2},"total":0} + validate_heartbeat( + heartbeat_events[0], + [ + { + "apispec": {}, + "hits": 1, + "hits_delta_since_sync": 1, + "method": "POST", + "path": "/app/create", + } + ], + {"aborted": 0, "attacksDetected": {"blocked": 2, "total": 2}, "total": 0}, ) From a2fd5f956670383cb99cf95604f731695c7d3b4e Mon Sep 17 00:00:00 2001 From: nadiraikido <166383531+nadiraikido@users.noreply.github.com> Date: Tue, 8 Apr 2025 13:35:33 +0200 Subject: [PATCH 02/18] Revert linting, add JSON endpoint in Django sample app --- end2end/django_mysql_test.py | 85 ++++++++------------ sample-apps/django-mysql/sample_app/urls.py | 1 + sample-apps/django-mysql/sample_app/views.py | 15 +++- 3 files changed, 48 insertions(+), 53 deletions(-) diff --git a/end2end/django_mysql_test.py b/end2end/django_mysql_test.py index 6f5db62c5..42c1b7e78 100644 --- a/end2end/django_mysql_test.py +++ b/end2end/django_mysql_test.py @@ -1,66 +1,53 @@ import time import pytest import requests -from .server.check_events_from_mock import ( - fetch_events_from_mock, - validate_started_event, - filter_on_event_type, - validate_heartbeat, -) +import json +from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type, validate_heartbeat # e2e tests for django_mysql sample app base_url_fw = "http://localhost:8080/app" base_url_nofw = "http://localhost:8081/app" - 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], ["django", "mysqlclient"]) - def test_safe_response_with_firewall(): dog_name = "Bobby Tables" - res = requests.post(base_url_fw + "/create", data={"dog_name": dog_name}) + res = requests.post(base_url_fw + "/create", data={'dog_name': dog_name}) assert res.status_code == 200 - def test_safe_response_without_firewall(): dog_name = "Bobby Tables" - res = requests.post(base_url_nofw + "/create", data={"dog_name": dog_name}) + res = requests.post(base_url_nofw + "/create", data={'dog_name': dog_name}) assert res.status_code == 200 def test_dangerous_response_with_firewall(): dog_name = 'Dangerous bobby", 1); -- ' - res = requests.post(base_url_fw + "/create", data={"dog_name": dog_name}) + res = requests.post(base_url_fw + "/create", data={'dog_name': dog_name}) assert res.status_code == 500 - time.sleep(5) # Wait for attack to be reported + 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": { - "sql": 'INSERT INTO sample_app_dogs (dog_name, dog_boss) VALUES ("Dangerous bobby", 1); -- ", "N/A")' - }, - "operation": "MySQLdb.Cursor.execute", - "pathToPayload": ".dog_name", - "payload": '"Dangerous bobby\\", 1); -- "', - "source": "body", - "user": None, + 'metadata': {'sql': 'INSERT INTO sample_app_dogs (dog_name, dog_boss) VALUES ("Dangerous bobby", 1); -- ", "N/A")'}, + 'operation': 'MySQLdb.Cursor.execute', + 'pathToPayload': '.dog_name', + 'payload': '"Dangerous bobby\\", 1); -- "', + 'source': "body", + 'user': None } - def test_dangerous_response_with_form_header_but_json_body(): - import json - headers = {"Content-Type": "application/x-www-form-urlencoded"} - json_body = json.dumps({"dog_name": 'Dangerous bobby", 1); -- '}) res = requests.post(base_url_fw + "/create", headers=headers, data=json_body) @@ -85,51 +72,45 @@ def test_dangerous_response_with_form_header_but_json_body(): "user": None, } - def test_dangerous_response_with_firewall_shell(): dog_name = 'Dangerous bobby", 1); -- ' res = requests.get(base_url_fw + "/shell/ls -la") assert res.status_code == 500 - time.sleep(5) # Wait for attack to be reported + 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) == 2 - del attacks[0] # Previous attack + del attacks[0] # Previous attack del attacks[0]["attack"]["stack"] assert attacks[0]["attack"] == { "blocked": True, "kind": "shell_injection", - "metadata": {"command": "ls -la"}, - "operation": "subprocess.Popen", - "pathToPayload": ".[0]", - "payload": '"ls -la"', - "source": "route_params", - "user": None, + 'metadata': {'command': 'ls -la'}, + 'operation': 'subprocess.Popen', + 'pathToPayload': '.[0]', + 'payload': '"ls -la"', + 'source': "route_params", + 'user': None } - def test_dangerous_response_without_firewall(): dog_name = 'Dangerous bobby", 1); -- ' - res = requests.post(base_url_nofw + "/create", data={"dog_name": dog_name}) + res = requests.post(base_url_nofw + "/create", data={'dog_name': dog_name}) assert res.status_code == 200 - def test_initial_heartbeat(): - time.sleep(55) # Sleep 5 + 55 seconds for heartbeat + time.sleep(55) # Sleep 5 + 55 seconds for heartbeat events = fetch_events_from_mock("http://localhost:5000") heartbeat_events = filter_on_event_type(events, "heartbeat") assert len(heartbeat_events) == 1 - validate_heartbeat( - heartbeat_events[0], - [ - { - "apispec": {}, - "hits": 1, - "hits_delta_since_sync": 1, - "method": "POST", - "path": "/app/create", - } - ], - {"aborted": 0, "attacksDetected": {"blocked": 2, "total": 2}, "total": 0}, + validate_heartbeat(heartbeat_events[0], + [{ + "apispec": {}, + "hits": 1, + "hits_delta_since_sync": 1, + "method": "POST", + "path": "/app/create" + }], + {"aborted":0,"attacksDetected":{"blocked":2,"total":2},"total":0} ) diff --git a/sample-apps/django-mysql/sample_app/urls.py b/sample-apps/django-mysql/sample_app/urls.py index 296442ee1..c7be2b8e8 100644 --- a/sample-apps/django-mysql/sample_app/urls.py +++ b/sample-apps/django-mysql/sample_app/urls.py @@ -5,6 +5,7 @@ urlpatterns = [ path("", views.index, name="index"), path("dogpage/", views.dog_page, name="dog_page"), + path("json/create", views.json_create_dog, name="json_create"), path("shell/", views.shell_url, name="shell"), path("create", views.create_dogpage, name="create") ] diff --git a/sample-apps/django-mysql/sample_app/views.py b/sample-apps/django-mysql/sample_app/views.py index 37e45702d..1eb7721ea 100644 --- a/sample-apps/django-mysql/sample_app/views.py +++ b/sample-apps/django-mysql/sample_app/views.py @@ -1,11 +1,12 @@ from django.shortcuts import render, get_object_or_404 -from django.http import HttpResponse +from django.http import HttpResponse, JsonResponse from django.template import loader from .models import Dogs from django.db import connection from django.views.decorators.csrf import csrf_exempt # Create your views here. import subprocess +import json def index(request): dogs = Dogs.objects.all() @@ -37,3 +38,15 @@ def create_dogpage(request): print("QUERY : ", query) cursor.execute(query) return HttpResponse("Dog page created") + +@csrf_exempt +def json_create_dog(request): + if request.method == 'POST': + body = request.body.decode('utf-8') + body_json = json.loads(body) + dog_name = body_json.get('dog_name') + + with connection.cursor() as cursor: + query = 'INSERT INTO sample_app_dogs (dog_name, dog_boss) VALUES ("%s", "N/A")' % dog_name + cursor.execute(query) + return JsonResponse({"status": "Dog page created"}) \ No newline at end of file From baeeb8e5371b2692e4be8b4aad4750fa7a79ebd4 Mon Sep 17 00:00:00 2001 From: nadiraikido <166383531+nadiraikido@users.noreply.github.com> Date: Tue, 8 Apr 2025 15:37:00 +0200 Subject: [PATCH 03/18] Always prioritize parsing JSON --- aikido_zen/sources/django/run_init_stage.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/aikido_zen/sources/django/run_init_stage.py b/aikido_zen/sources/django/run_init_stage.py index 15493a905..887286af0 100644 --- a/aikido_zen/sources/django/run_init_stage.py +++ b/aikido_zen/sources/django/run_init_stage.py @@ -10,20 +10,19 @@ def run_init_stage(request): """Parse request and body, run "init" stage with request_handler""" body = None try: - # Check for JSON - if body is None and request.content_type == "application/json": - try: - body = json.loads(request.body) - except Exception: - pass - - # try-catch loading of form parameters, this is to fix issue with DATA_UPLOAD_MAX_NUMBER_FIELDS : try: - body = request.POST.dict() - if len(body) == 0: - body = None # Reset + body = json.loads(request.body) except Exception: pass + + if body is None or len(body) == 0: + # try-catch loading of form parameters, this is to fix issue with DATA_UPLOAD_MAX_NUMBER_FIELDS : + try: + body = request.POST.dict() + if len(body) == 0: + body = None # Reset + except Exception: + pass if body is None or len(body) == 0: # E.g. XML Data From df376a35b8373e6c3842840cc38f023ddc6d6dfb Mon Sep 17 00:00:00 2001 From: nadiraikido <166383531+nadiraikido@users.noreply.github.com> Date: Tue, 8 Apr 2025 15:41:49 +0200 Subject: [PATCH 04/18] Linting --- aikido_zen/sources/django/run_init_stage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aikido_zen/sources/django/run_init_stage.py b/aikido_zen/sources/django/run_init_stage.py index 887286af0..6b8625b16 100644 --- a/aikido_zen/sources/django/run_init_stage.py +++ b/aikido_zen/sources/django/run_init_stage.py @@ -14,10 +14,10 @@ def run_init_stage(request): body = json.loads(request.body) except Exception: pass - + if body is None or len(body) == 0: # try-catch loading of form parameters, this is to fix issue with DATA_UPLOAD_MAX_NUMBER_FIELDS : - try: + try: body = request.POST.dict() if len(body) == 0: body = None # Reset From 10f9c2460c46159ae0a3c81ac6c6107bd2122f10 Mon Sep 17 00:00:00 2001 From: kapyteinaikido <166383531+kapyteinaikido@users.noreply.github.com> Date: Tue, 8 Apr 2025 15:43:26 +0200 Subject: [PATCH 05/18] Update django_mysql_test.py --- end2end/django_mysql_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/end2end/django_mysql_test.py b/end2end/django_mysql_test.py index 42c1b7e78..a1f2146bc 100644 --- a/end2end/django_mysql_test.py +++ b/end2end/django_mysql_test.py @@ -50,7 +50,7 @@ def test_dangerous_response_with_form_header_but_json_body(): headers = {"Content-Type": "application/x-www-form-urlencoded"} json_body = json.dumps({"dog_name": 'Dangerous bobby", 1); -- '}) - res = requests.post(base_url_fw + "/create", headers=headers, data=json_body) + res = requests.post(base_url_fw + "/json/create", headers=headers, data=json_body) assert res.status_code == 500 time.sleep(5) From 137100918a82fb3b80af547c78155d3e58ee303a Mon Sep 17 00:00:00 2001 From: nadiraikido <166383531+nadiraikido@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:25:52 +0200 Subject: [PATCH 06/18] Improve incorrect test --- .../sources/django/run_init_stage_test.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/aikido_zen/sources/django/run_init_stage_test.py b/aikido_zen/sources/django/run_init_stage_test.py index 497bdd0da..60796336b 100644 --- a/aikido_zen/sources/django/run_init_stage_test.py +++ b/aikido_zen/sources/django/run_init_stage_test.py @@ -40,6 +40,18 @@ def mock_request(): return request +@pytest.fixture +def mock_request_form_body(): + """Fixture to create a mock request object.""" + request = MagicMock() + request.POST.dict.return_value = {"a": [1, 2], "b": [2, 3]} + request.content_type = "application/x-www-form-urlencoded" + request.body = "a[0]=1&a[1]=2&b[0]=2&b[1]=3" # Example JSON body + request.META = wsgi_request + request.scope = None + return request + + @pytest.fixture(autouse=True) def run_around_tests(): yield @@ -57,10 +69,9 @@ def test_run_init_stage_with_json(mock_request): assert {"key": "value"} == context.body -def test_run_init_stage_with_dict(mock_request): +def test_run_init_stage_with_dict(mock_request_form_body): """Test run_init_stage with a JSON request.""" - mock_request.POST.dict.return_value = {"a": [1, 2], "b": [2, 3]} - run_init_stage(mock_request) + run_init_stage(mock_request_form_body) # Assertions context: Context = get_current_context() From 2fcca894dba5db3fd99972a000af2d5ee901bad3 Mon Sep 17 00:00:00 2001 From: nadiraikido <166383531+nadiraikido@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:34:23 +0200 Subject: [PATCH 07/18] Fix assertions --- end2end/django_mysql_test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/end2end/django_mysql_test.py b/end2end/django_mysql_test.py index a1f2146bc..a25f094ff 100644 --- a/end2end/django_mysql_test.py +++ b/end2end/django_mysql_test.py @@ -57,7 +57,8 @@ def test_dangerous_response_with_form_header_but_json_body(): events = fetch_events_from_mock("http://localhost:5000") attacks = filter_on_event_type(events, "detected_attack") - assert len(attacks) == 1 + assert len(attacks) == 2 + del attacks[0] # Previous attack del attacks[0]["attack"]["stack"] assert attacks[0]["attack"] == { "blocked": True, @@ -80,7 +81,7 @@ def test_dangerous_response_with_firewall_shell(): events = fetch_events_from_mock("http://localhost:5000") attacks = filter_on_event_type(events, "detected_attack") - assert len(attacks) == 2 + assert len(attacks) == 3 del attacks[0] # Previous attack del attacks[0]["attack"]["stack"] assert attacks[0]["attack"] == { From 20d6c5bf91938f3597ee3903a298fc1c5f495918 Mon Sep 17 00:00:00 2001 From: nadiraikido <166383531+nadiraikido@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:44:55 +0200 Subject: [PATCH 08/18] Delete attacks after they happened --- end2end/django_mysql_test.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/end2end/django_mysql_test.py b/end2end/django_mysql_test.py index a25f094ff..5309795cf 100644 --- a/end2end/django_mysql_test.py +++ b/end2end/django_mysql_test.py @@ -45,6 +45,7 @@ def test_dangerous_response_with_firewall(): 'source': "body", 'user': None } + del attacks[0] def test_dangerous_response_with_form_header_but_json_body(): headers = {"Content-Type": "application/x-www-form-urlencoded"} @@ -57,8 +58,7 @@ def test_dangerous_response_with_form_header_but_json_body(): events = fetch_events_from_mock("http://localhost:5000") attacks = filter_on_event_type(events, "detected_attack") - assert len(attacks) == 2 - del attacks[0] # Previous attack + assert len(attacks) == 1 del attacks[0]["attack"]["stack"] assert attacks[0]["attack"] == { "blocked": True, @@ -72,6 +72,7 @@ def test_dangerous_response_with_form_header_but_json_body(): "source": "body", "user": None, } + del attacks[0] def test_dangerous_response_with_firewall_shell(): dog_name = 'Dangerous bobby", 1); -- ' @@ -81,8 +82,7 @@ def test_dangerous_response_with_firewall_shell(): events = fetch_events_from_mock("http://localhost:5000") attacks = filter_on_event_type(events, "detected_attack") - assert len(attacks) == 3 - del attacks[0] # Previous attack + assert len(attacks) == 1 del attacks[0]["attack"]["stack"] assert attacks[0]["attack"] == { "blocked": True, @@ -94,6 +94,7 @@ def test_dangerous_response_with_firewall_shell(): 'source': "route_params", 'user': None } + del attacks[0] def test_dangerous_response_without_firewall(): dog_name = 'Dangerous bobby", 1); -- ' From 3331bc880a16bdc7aed44bb11dbf09de37bff11f Mon Sep 17 00:00:00 2001 From: nadiraikido <166383531+nadiraikido@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:54:36 +0200 Subject: [PATCH 09/18] Revert as attacks list is not persisted --- end2end/django_mysql_test.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/end2end/django_mysql_test.py b/end2end/django_mysql_test.py index 5309795cf..9437b837f 100644 --- a/end2end/django_mysql_test.py +++ b/end2end/django_mysql_test.py @@ -58,9 +58,9 @@ def test_dangerous_response_with_form_header_but_json_body(): 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"] == { + assert len(attacks) == 2 + del attacks[1]["attack"]["stack"] + assert attacks[1]["attack"] == { "blocked": True, "kind": "sql_injection", "metadata": { @@ -72,7 +72,6 @@ def test_dangerous_response_with_form_header_but_json_body(): "source": "body", "user": None, } - del attacks[0] def test_dangerous_response_with_firewall_shell(): dog_name = 'Dangerous bobby", 1); -- ' @@ -82,9 +81,9 @@ def test_dangerous_response_with_firewall_shell(): 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"] == { + assert len(attacks) == 3 + del attacks[2]["attack"]["stack"] + assert attacks[2]["attack"] == { "blocked": True, "kind": "shell_injection", 'metadata': {'command': 'ls -la'}, @@ -94,7 +93,6 @@ def test_dangerous_response_with_firewall_shell(): 'source': "route_params", 'user': None } - del attacks[0] def test_dangerous_response_without_firewall(): dog_name = 'Dangerous bobby", 1); -- ' From d9fa71105c4672f96238fd018e834c8743277c02 Mon Sep 17 00:00:00 2001 From: nadiraikido <166383531+nadiraikido@users.noreply.github.com> Date: Tue, 8 Apr 2025 19:04:13 +0200 Subject: [PATCH 10/18] Increase number of attacks --- end2end/django_mysql_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/end2end/django_mysql_test.py b/end2end/django_mysql_test.py index 9437b837f..bd987b385 100644 --- a/end2end/django_mysql_test.py +++ b/end2end/django_mysql_test.py @@ -112,5 +112,5 @@ def test_initial_heartbeat(): "method": "POST", "path": "/app/create" }], - {"aborted":0,"attacksDetected":{"blocked":2,"total":2},"total":0} + {"aborted":0,"attacksDetected":{"blocked":3,"total":3},"total":0} ) From 5a97992442e7e79a73a5782427b7f18207ffdaa4 Mon Sep 17 00:00:00 2001 From: nadiraikido <166383531+nadiraikido@users.noreply.github.com> Date: Wed, 9 Apr 2025 17:42:52 +0200 Subject: [PATCH 11/18] Apply the same prioritization for Quart --- aikido_zen/sources/quart.py | 16 +++++++++----- end2end/quart_postgres_uvicorn_test.py | 26 +++++++++++++++++++++++ sample-apps/quart-postgres-uvicorn/app.py | 18 ++++++++++++++++ 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/aikido_zen/sources/quart.py b/aikido_zen/sources/quart.py index a2057362f..cf0636963 100644 --- a/aikido_zen/sources/quart.py +++ b/aikido_zen/sources/quart.py @@ -36,14 +36,20 @@ async def handle_request_wrapper(former_handle_request, quart_app, req): try: context = get_current_context() if context: + body = None + try: + body = await req.get_json(force=True) + except Exception: + pass + form = await req.form - if req.is_json: - context.set_body(await req.get_json()) - elif form: - context.set_body(form) + if form and body is None: + body = form else: data = await req.data - context.set_body(data.decode("utf-8")) + body = data.decode("utf-8") + + context.set_body(body) context.cookies = req.cookies.to_dict() context.set_as_current_context() except Exception as e: diff --git a/end2end/quart_postgres_uvicorn_test.py b/end2end/quart_postgres_uvicorn_test.py index ebea4a9ab..563c95e4e 100644 --- a/end2end/quart_postgres_uvicorn_test.py +++ b/end2end/quart_postgres_uvicorn_test.py @@ -1,5 +1,6 @@ import time import pytest +import json import requests from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type @@ -45,6 +46,31 @@ def test_dangerous_response_with_firewall(): assert attacks[0]["attack"]["user"]["id"] == "user123" assert attacks[0]["attack"]["user"]["name"] == "John Doe" +def test_dangerous_response_with_form_header_but_json_body(): + headers = {"Content-Type": "application/x-www-form-urlencoded"} + json_body = json.dumps({"dog_name": 'Dangerous bobby", 1); -- '}) + + res = requests.post(post_url_fw + "/json/create", headers=headers, data=json_body) + assert res.status_code == 500 + time.sleep(5) + + events = fetch_events_from_mock("http://localhost:5000") + attacks = filter_on_event_type(events, "detected_attack") + + assert len(attacks) == 2 + del attacks[1]["attack"]["stack"] + assert attacks[1]["attack"] == { + "blocked": True, + "kind": "sql_injection", + "metadata": { + "sql": 'INSERT INTO sample_app_dogs (dog_name, dog_boss) VALUES ("Dangerous bobby", 1); -- ", "N/A")' + }, + "operation": "asyncpg.connection.Connection.execute", + "pathToPayload": ".dog_name", + "payload": '"Dangerous bobby\\", 1); -- "', + "source": "body", + "user": None, + } def test_dangerous_response_without_firewall(): dog_name = "Dangerous Bobby', TRUE); -- " diff --git a/sample-apps/quart-postgres-uvicorn/app.py b/sample-apps/quart-postgres-uvicorn/app.py index 65b59725b..41e1c39f6 100644 --- a/sample-apps/quart-postgres-uvicorn/app.py +++ b/sample-apps/quart-postgres-uvicorn/app.py @@ -64,6 +64,24 @@ async def create_dog(): return jsonify({"message": f'Dog {dog_name} created successfully'}), 201 +@app.route("/json/create", methods=['POST']) +async def create_dog_json(): + data = await request.get_json(force=True) + dog_name = data.get('dog_name') + + if not dog_name: + return jsonify({"error": "dog_name is required"}), 400 + + conn = await get_db_connection() + try: + await conn.execute( + f"INSERT INTO dogs (dog_name, isAdmin) VALUES ('%s', FALSE)" % dog_name + ) + finally: + await conn.close() + + return jsonify({"message": f'Dog {dog_name} created successfully'}), 201 + @app.route("/create_many", methods=['POST']) async def create_dog_many(): data = await request.form From 3e29cfaaff271dda7ec4416a268a84265bf2aa1b Mon Sep 17 00:00:00 2001 From: nadiraikido <166383531+nadiraikido@users.noreply.github.com> Date: Wed, 9 Apr 2025 17:44:51 +0200 Subject: [PATCH 12/18] Remove unnecessary del --- end2end/django_mysql_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/end2end/django_mysql_test.py b/end2end/django_mysql_test.py index bd987b385..efe90be0b 100644 --- a/end2end/django_mysql_test.py +++ b/end2end/django_mysql_test.py @@ -45,7 +45,6 @@ def test_dangerous_response_with_firewall(): 'source': "body", 'user': None } - del attacks[0] def test_dangerous_response_with_form_header_but_json_body(): headers = {"Content-Type": "application/x-www-form-urlencoded"} From 97b1827d9e6de93b4a8e6dede6669df374709619 Mon Sep 17 00:00:00 2001 From: nadiraikido <166383531+nadiraikido@users.noreply.github.com> Date: Wed, 9 Apr 2025 17:48:50 +0200 Subject: [PATCH 13/18] Update route --- end2end/quart_postgres_uvicorn_test.py | 2 +- sample-apps/quart-postgres-uvicorn/app.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/end2end/quart_postgres_uvicorn_test.py b/end2end/quart_postgres_uvicorn_test.py index 563c95e4e..39480069a 100644 --- a/end2end/quart_postgres_uvicorn_test.py +++ b/end2end/quart_postgres_uvicorn_test.py @@ -50,7 +50,7 @@ def test_dangerous_response_with_form_header_but_json_body(): headers = {"Content-Type": "application/x-www-form-urlencoded"} json_body = json.dumps({"dog_name": 'Dangerous bobby", 1); -- '}) - res = requests.post(post_url_fw + "/json/create", headers=headers, data=json_body) + res = requests.post(post_url_fw + "/json", headers=headers, data=json_body) assert res.status_code == 500 time.sleep(5) diff --git a/sample-apps/quart-postgres-uvicorn/app.py b/sample-apps/quart-postgres-uvicorn/app.py index 41e1c39f6..e02d48b1e 100644 --- a/sample-apps/quart-postgres-uvicorn/app.py +++ b/sample-apps/quart-postgres-uvicorn/app.py @@ -64,7 +64,7 @@ async def create_dog(): return jsonify({"message": f'Dog {dog_name} created successfully'}), 201 -@app.route("/json/create", methods=['POST']) +@app.route("/create/json", methods=['POST']) async def create_dog_json(): data = await request.get_json(force=True) dog_name = data.get('dog_name') From 97549739a1809e30904d18a5a511868eb04b8b97 Mon Sep 17 00:00:00 2001 From: kapyteinaikido <166383531+kapyteinaikido@users.noreply.github.com> Date: Fri, 11 Apr 2025 18:52:47 +0200 Subject: [PATCH 14/18] Change payload --- end2end/quart_postgres_uvicorn_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/end2end/quart_postgres_uvicorn_test.py b/end2end/quart_postgres_uvicorn_test.py index 39480069a..d8a73eae0 100644 --- a/end2end/quart_postgres_uvicorn_test.py +++ b/end2end/quart_postgres_uvicorn_test.py @@ -48,7 +48,7 @@ def test_dangerous_response_with_firewall(): def test_dangerous_response_with_form_header_but_json_body(): headers = {"Content-Type": "application/x-www-form-urlencoded"} - json_body = json.dumps({"dog_name": 'Dangerous bobby", 1); -- '}) + json_body = json.dumps({"dog_name": "Dangerous bobby', 1); -- "}) res = requests.post(post_url_fw + "/json", headers=headers, data=json_body) assert res.status_code == 500 From a533ca106b0693acb0da1bb459568aa67d380872 Mon Sep 17 00:00:00 2001 From: kapyteinaikido <166383531+kapyteinaikido@users.noreply.github.com> Date: Fri, 11 Apr 2025 18:53:38 +0200 Subject: [PATCH 15/18] Update quart_postgres_uvicorn_test.py --- end2end/quart_postgres_uvicorn_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/end2end/quart_postgres_uvicorn_test.py b/end2end/quart_postgres_uvicorn_test.py index d8a73eae0..930248fdb 100644 --- a/end2end/quart_postgres_uvicorn_test.py +++ b/end2end/quart_postgres_uvicorn_test.py @@ -67,7 +67,7 @@ def test_dangerous_response_with_form_header_but_json_body(): }, "operation": "asyncpg.connection.Connection.execute", "pathToPayload": ".dog_name", - "payload": '"Dangerous bobby\\", 1); -- "', + "payload": '"Dangerous bobby\\', 1); -- "', "source": "body", "user": None, } From 74d3b04459a2fb71bc11b2ae4a2b8525f11192cd Mon Sep 17 00:00:00 2001 From: kapyteinaikido <166383531+kapyteinaikido@users.noreply.github.com> Date: Fri, 11 Apr 2025 18:57:50 +0200 Subject: [PATCH 16/18] Correct payload --- end2end/quart_postgres_uvicorn_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/end2end/quart_postgres_uvicorn_test.py b/end2end/quart_postgres_uvicorn_test.py index 930248fdb..ea7d6ccec 100644 --- a/end2end/quart_postgres_uvicorn_test.py +++ b/end2end/quart_postgres_uvicorn_test.py @@ -67,7 +67,7 @@ def test_dangerous_response_with_form_header_but_json_body(): }, "operation": "asyncpg.connection.Connection.execute", "pathToPayload": ".dog_name", - "payload": '"Dangerous bobby\\', 1); -- "', + "payload": '"Dangerous bobby\', 1); -- "', "source": "body", "user": None, } From 9804b9702da05b5f068f54485e69200f1dcfed1a Mon Sep 17 00:00:00 2001 From: kapyteinaikido <166383531+kapyteinaikido@users.noreply.github.com> Date: Mon, 14 Apr 2025 16:30:51 +0200 Subject: [PATCH 17/18] Update assertion --- end2end/quart_postgres_uvicorn_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/end2end/quart_postgres_uvicorn_test.py b/end2end/quart_postgres_uvicorn_test.py index ea7d6ccec..9636f22d9 100644 --- a/end2end/quart_postgres_uvicorn_test.py +++ b/end2end/quart_postgres_uvicorn_test.py @@ -63,7 +63,7 @@ def test_dangerous_response_with_form_header_but_json_body(): "blocked": True, "kind": "sql_injection", "metadata": { - "sql": 'INSERT INTO sample_app_dogs (dog_name, dog_boss) VALUES ("Dangerous bobby", 1); -- ", "N/A")' + "sql": "INSERT INTO dogs (dog_name, isAdmin) VALUES ('Dangerous bobby', 1); -- ', FALSE)" }, "operation": "asyncpg.connection.Connection.execute", "pathToPayload": ".dog_name", From 537fbaf9f4ef66fe180d8f0c5e8e93dcae6baaae Mon Sep 17 00:00:00 2001 From: kapyteinaikido <166383531+kapyteinaikido@users.noreply.github.com> Date: Mon, 14 Apr 2025 17:01:44 +0200 Subject: [PATCH 18/18] Change assertion again --- end2end/quart_postgres_uvicorn_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/end2end/quart_postgres_uvicorn_test.py b/end2end/quart_postgres_uvicorn_test.py index 9636f22d9..75d13ea17 100644 --- a/end2end/quart_postgres_uvicorn_test.py +++ b/end2end/quart_postgres_uvicorn_test.py @@ -69,7 +69,7 @@ def test_dangerous_response_with_form_header_but_json_body(): "pathToPayload": ".dog_name", "payload": '"Dangerous bobby\', 1); -- "', "source": "body", - "user": None, + "user": {'id': 'user123', 'lastIpAddress': '127.0.0.1', 'name': 'John Doe'}, } def test_dangerous_response_without_firewall():