From 7c2ca78c077c738fcc1f2d596fd246832e60b3ab Mon Sep 17 00:00:00 2001 From: BitterPanda63 Date: Fri, 7 Mar 2025 13:34:52 +0100 Subject: [PATCH 01/31] port code from java e2e PR --- end2end/__init__.py | 4 + end2end/server/check_events_from_mock.py | 27 --- end2end/server/mock_aikido_core.py | 6 + end2end/utils/EventHandler.py | 20 +++ end2end/utils/__init__.py | 61 +++++++ end2end/utils/assert_equals.py | 4 + end2end/utils/request.py | 29 ++++ end2end/utils/test_bot_blocking.py | 36 ++++ end2end/utils/test_ip_blocking.py | 41 +++++ end2end/utils/test_payloads_safe_vs_unsafe.py | 13 ++ end2end/utils/test_ratelimiting.py | 160 ++++++++++++++++++ 11 files changed, 374 insertions(+), 27 deletions(-) delete mode 100644 end2end/server/check_events_from_mock.py create mode 100644 end2end/utils/EventHandler.py create mode 100644 end2end/utils/__init__.py create mode 100644 end2end/utils/assert_equals.py create mode 100644 end2end/utils/request.py create mode 100644 end2end/utils/test_bot_blocking.py create mode 100644 end2end/utils/test_ip_blocking.py create mode 100644 end2end/utils/test_payloads_safe_vs_unsafe.py create mode 100644 end2end/utils/test_ratelimiting.py diff --git a/end2end/__init__.py b/end2end/__init__.py index e69de29b..ec6ca485 100644 --- a/end2end/__init__.py +++ b/end2end/__init__.py @@ -0,0 +1,4 @@ +import json + +with open('end2end/attack_events.json', 'r') as file: + events = json.load(file) diff --git a/end2end/server/check_events_from_mock.py b/end2end/server/check_events_from_mock.py deleted file mode 100644 index af8724be..00000000 --- a/end2end/server/check_events_from_mock.py +++ /dev/null @@ -1,27 +0,0 @@ -import requests -import json - -def fetch_events_from_mock(url): - mock_events_url = f"{url}/mock/events" - res = requests.get(mock_events_url, timeout=5) - json_events = json.loads(res.content.decode("utf-8")) - return json_events - -def filter_on_event_type(events, type): - return [event for event in events if event["type"] == type] - -def validate_started_event(event, stack, dry_mode=False, serverless=False, os_name="Linux", platform="CPython"): - assert event["agent"]["dryMode"] == dry_mode - assert event["agent"]["library"] == "firewall-python" - assert event["agent"]["nodeEnv"] == "" - assert event["agent"]["os"]["name"] == os_name - assert event["agent"]["platform"]["name"] == platform - assert event["agent"]["serverless"] == serverless - # # Check for packages is disabled until we start using them in core : - # if stack is not None: - # assert set(event["agent"]["stack"]) == set(stack) - -def validate_heartbeat(event, routes, req_stats): - assert event["type"] == "heartbeat" - assert event["routes"] == routes - assert event["stats"]["requests"] == req_stats diff --git a/end2end/server/mock_aikido_core.py b/end2end/server/mock_aikido_core.py index cb7f2567..110763ba 100644 --- a/end2end/server/mock_aikido_core.py +++ b/end2end/server/mock_aikido_core.py @@ -82,6 +82,12 @@ def mock_set_config(): def mock_get_events(): return jsonify(events) +@app.route('/mock/reset', methods=['GET']) +def mock_reset(): + global events + events = [] # Reset events + return jsonify({}) + if __name__ == '__main__': if len(sys.argv) < 2 or len(sys.argv) > 3: diff --git a/end2end/utils/EventHandler.py b/end2end/utils/EventHandler.py new file mode 100644 index 00000000..d0ccfa61 --- /dev/null +++ b/end2end/utils/EventHandler.py @@ -0,0 +1,20 @@ +import time +import requests +import json + +class EventHandler: + def __init__(self, url="http://localhost:5000"): + self.url = url + def reset(self): + print("Resetting stored events on mock server") + res = requests.get(self.url + "/mock/reset", timeout=5) + time.sleep(1) + def fetch_events_from_mock(self): + res = requests.get(self.url + "/mock/events", timeout=5) + json_events = json.loads(res.content.decode("utf-8")) + return json_events + def fetch_attacks(self): + return filter_on_event_type(self.fetch_events_from_mock(), "detected_attack") + +def filter_on_event_type(events, type): + return [event for event in events if event["type"] == type] \ No newline at end of file diff --git a/end2end/utils/__init__.py b/end2end/utils/__init__.py new file mode 100644 index 00000000..91f834af --- /dev/null +++ b/end2end/utils/__init__.py @@ -0,0 +1,61 @@ +import time + +from .EventHandler import EventHandler +from .assert_equals import assert_eq +from .request import Request +from .test_bot_blocking import test_bot_blocking +from .test_ip_blocking import test_ip_blocking +from .test_ratelimiting import test_ratelimiting, test_ratelimiting_per_user +from .test_payloads_safe_vs_unsafe import test_payloads_safe_vs_unsafe + +class App: + def __init__(self, port): + self.urls = { + "enabled": f"http://localhost:{port}", + "disabled": f"http://localhost:{port + 1}" + } + self.payloads = {} + self.event_handler = EventHandler() + + def add_payload(self,key, safe_request, unsafe_request, test_event=None): + self.payloads[key] = { + "safe": safe_request, + "unsafe": unsafe_request, + "test_event": test_event + } + + def test_payload(self, key): + if key not in self.payloads: + raise Exception("Payload " + key + " not found.") + payload = self.payloads.get(key) + + self.event_handler.reset() + test_payloads_safe_vs_unsafe(payload, self.urls) + print("✅ Tested payload: " + key) + + if payload["test_event"]: + time.sleep(5) + attacks = self.event_handler.fetch_attacks() + assert_eq(len(attacks), equals=1) + for k, v in payload["test_event"].items(): + if k == "user_id": # exemption rule for user ids + assert_eq(attacks[0]["attack"]["user"]["id"], v) + else: + assert_eq(attacks[0]["attack"][k], equals=v) + print("✅ Tested accurate event reporting for: " + key) + + def test_all_payloads(self): + for key in self.payloads.keys(): + self.test_payload(key) + + def test_blocking(self): + test_bot_blocking(self.urls["enabled"]) + print("✅ Tested bot blocking") + test_ip_blocking(self.urls["enabled"]) + print("✅ Tested IP Blocking") + + def test_rate_limiting(self, route="/test_ratelimiting_1"): + test_ratelimiting(self.urls["enabled"] + route) + print("✅ Tested rate-limiting") + test_ratelimiting_per_user(self.urls["enabled"] + route) + print("✅ Tested rate-limiting (User)") diff --git a/end2end/utils/assert_equals.py b/end2end/utils/assert_equals.py new file mode 100644 index 00000000..70260a12 --- /dev/null +++ b/end2end/utils/assert_equals.py @@ -0,0 +1,4 @@ +def assert_eq(val1, equals, val2=None): + assert val1 == equals, f"Assertion failed: Expected {equals} != {val1}" + if val2 is not None: + assert val2 == equals, f"Assertion failed: Expected {equals} != {val2}" \ No newline at end of file diff --git a/end2end/utils/request.py b/end2end/utils/request.py new file mode 100644 index 00000000..76a24562 --- /dev/null +++ b/end2end/utils/request.py @@ -0,0 +1,29 @@ +import requests + +class Request: + def __init__(self, route, method='POST', headers=None, data_type='json', body=None): + self.method = method + self.route = route + self.headers = headers if headers is not None else {} + self.data_type = data_type # 'json' or 'form' + self.body = body + + def execute(self, base_url): + """Execute the request and return the status code.""" + url = f"{base_url}{self.route}" + + if self.method.upper() == 'POST': + if self.data_type == 'json': + response = requests.post(url, json=self.body, headers=self.headers) + elif self.data_type == 'form': + response = requests.post(url, data=self.body, headers=self.headers) + else: + raise ValueError("Unsupported data type. Use 'json' or 'form'.") + elif self.method.upper() == 'GET': + response = requests.get(url, headers=self.headers) + else: + raise ValueError("Unsupported HTTP method. Use 'GET' or 'POST'.") + + return response.status_code + def __str__(self): + return f"Request(method={self.method}, route={self.route}, headers={self.headers}, data_type={self.data_type}, body={self.body})" diff --git a/end2end/utils/test_bot_blocking.py b/end2end/utils/test_bot_blocking.py new file mode 100644 index 00000000..12509786 --- /dev/null +++ b/end2end/utils/test_bot_blocking.py @@ -0,0 +1,36 @@ +import requests +from utils.assert_equals import assert_eq + +def test_bot_blocking(url): + # Allowed User-Agents : + res = requests.get(url, headers={ + 'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + }) + assert_eq(res.status_code, equals=200) + res = requests.get(url, headers={ + 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1" + }) + assert_eq(res.status_code, equals=200) + res = requests.get(url, headers={ + 'User-Agent': "Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/14.0 Chrome/91.0.4472.114 Mobile Safari/537.36" + }) + assert_eq(res.status_code, equals=200) + + # Blocked User-Agent : + res = requests.get(url, headers={ + 'User-Agent': "BYTESPIDER" + }) + assert_eq(res.status_code, equals=403) + assert_eq(res.text, equals="You are not allowed to access this resource because you have been identified as a bot.") + + # More complex blocked User-Agent : + res = requests.get(url, headers={ + 'User-Agent': "Mozilla/5.0 (compatible; Bytespider/1.0; +http://bytespider.com/bot.html)" + }) + assert_eq(res.status_code, equals=403) + assert_eq(res.text, equals="You are not allowed to access this resource because you have been identified as a bot.") + res = requests.get(url, headers={ + 'User-Agent': "Mozilla/5.0 (compatible; AI2Bot/1.0; +http://www.aaai.org/Press/Reports/AI2Bot)" + }) + assert_eq(res.status_code, equals=403) + assert_eq(res.text, equals="You are not allowed to access this resource because you have been identified as a bot.") diff --git a/end2end/utils/test_ip_blocking.py b/end2end/utils/test_ip_blocking.py new file mode 100644 index 00000000..417a3c7e --- /dev/null +++ b/end2end/utils/test_ip_blocking.py @@ -0,0 +1,41 @@ +import requests +from utils.assert_equals import assert_eq + +def test_ip_blocking(url): + # Allowed IP : + res = requests.get(url, headers={ + 'X-Forwarded-For': "192.168.1.1" + }) + assert_eq(res.status_code, equals=200) + + # Blocked IP : + res = requests.get(url, headers={ + 'X-Forwarded-For': "1.2.3.4" + }) + assert_eq(res.status_code, equals=403) + assert_eq(res.text, equals="Your IP address is not allowed to access this resource. (Your IP: 1.2.3.4)") + + # More complex X-Forwarded-For : + res = requests.get(url, headers={ + 'X-Forwarded-For': "invalid.ip.here.now, 1.2.3.4 " + }) + assert_eq(res.status_code, equals=403) + assert_eq(res.text, equals="Your IP address is not allowed to access this resource. (Your IP: 1.2.3.4)") + + # More complex but safe X-Forwarded-For : + res = requests.get(url, headers={ + 'X-Forwarded-For': "invalid.ip.here.now, 192.168.1.1 " + }) + assert_eq(res.status_code, equals=200) + + # It should only use the first valid ip + res = requests.get(url, headers={ + 'X-Forwarded-For': "192.168.1.1, 1.2.3.4" + }) + assert_eq(res.status_code, equals=200) + + # It should work with an empty X-Forwarded-For + res = requests.get(url, headers={ + 'X-Forwarded-For': "" + }) + assert_eq(res.status_code, equals=200) \ No newline at end of file diff --git a/end2end/utils/test_payloads_safe_vs_unsafe.py b/end2end/utils/test_payloads_safe_vs_unsafe.py new file mode 100644 index 00000000..d88e7dd3 --- /dev/null +++ b/end2end/utils/test_payloads_safe_vs_unsafe.py @@ -0,0 +1,13 @@ +from . import assert_eq + +def test_payloads_safe_vs_unsafe(payloads, urls): + print("Safe req to : (1) " + urls["enabled"] + payloads["safe"].route) + assert_eq(val1=payloads["safe"].execute(urls["enabled"]), equals=200) + + print("Safe req to : (0) " + urls["disabled"] + payloads["safe"].route) + assert_eq(val1=payloads["safe"].execute(urls["disabled"]), equals=200) + + print("Unsafe req to : (1) " + urls["enabled"] + payloads["unsafe"].route) + assert_eq(val1=payloads["unsafe"].execute(urls["enabled"]), equals=500) + print("Unsafe req to : (0) " + urls["disabled"] + payloads["unsafe"].route) + assert_eq(val1=payloads["unsafe"].execute(urls["disabled"]), equals=200) \ No newline at end of file diff --git a/end2end/utils/test_ratelimiting.py b/end2end/utils/test_ratelimiting.py new file mode 100644 index 00000000..a06765c0 --- /dev/null +++ b/end2end/utils/test_ratelimiting.py @@ -0,0 +1,160 @@ +import time +import requests +from utils.assert_equals import assert_eq + +def test_ratelimiting(url): + # Test route (First req & 2nd Req) : + res = requests.get(url, headers={ + 'X-Forwarded-For': "192.168.1.1" + }) + assert_eq(res.status_code, equals=200) + res = requests.get(url, headers={ + 'X-Forwarded-For': "192.168.1.1" + }) + assert_eq(res.status_code, equals=200) + + # 3rd & 4th (blocked) requests : + res = requests.get(url, headers={ + 'X-Forwarded-For': "192.168.1.1" + }) + assert_eq(res.status_code, equals=429) + assert_eq(res.text, equals="You are rate limited by Zen. (Your IP: 192.168.1.1)") + res = requests.get(url, headers={ + 'X-Forwarded-For': "192.168.1.1" + }) + assert_eq(res.status_code, equals=429) + assert_eq(res.text, equals="You are rate limited by Zen. (Your IP: 192.168.1.1)") + + # Now do the same but with a different IP in the same time block : + res = requests.get(url, headers={ + 'X-Forwarded-For': "192.168.1.2" + }) + assert_eq(res.status_code, equals=200) + res = requests.get(url, headers={ + 'X-Forwarded-For': "192.168.1.2" + }) + assert_eq(res.status_code, equals=200) + res = requests.get(url, headers={ + 'X-Forwarded-For': "192.168.1.2" + }) + assert_eq(res.status_code, equals=429) + assert_eq(res.text, equals="You are rate limited by Zen. (Your IP: 192.168.1.2)") + res = requests.get(url, headers={ + 'X-Forwarded-For': "192.168.1.2" + }) + assert_eq(res.status_code, equals=429) + assert_eq(res.text, equals="You are rate limited by Zen. (Your IP: 192.168.1.2)") + + # Now wait 5 seconds so your window is over and re-request : + time.sleep(5) + res = requests.get(url, headers={ + 'X-Forwarded-For': "192.168.1.2" + }) + assert_eq(res.status_code, equals=200) + res = requests.get(url, headers={ + 'X-Forwarded-For': "192.168.1.1" + }) + assert_eq(res.status_code, equals=200) + res = requests.get(url, headers={ + 'X-Forwarded-For': "192.168.1.2" + }) + assert_eq(res.status_code, equals=200) + assert_eq(res.status_code, equals=200) + res = requests.get(url, headers={ + 'X-Forwarded-For': "192.168.1.1" + }) + assert_eq(res.status_code, equals=200) + + +def test_ratelimiting_per_user(url): + # Test route (First req & 2nd Req) : + res = requests.get(url, headers={ + 'user': 'id1' + }) + assert_eq(res.status_code, equals=200) + res = requests.get(url, headers={ + 'user': 'id1' + }) + assert_eq(res.status_code, equals=200) + + # 3rd & 4th (blocked) requests : + res = requests.get(url, headers={ + 'user': 'id1' + }) + assert_eq(res.status_code, equals=429) + assert_eq(res.text, equals="You are rate limited by Zen.") + res = requests.get(url, headers={ + 'user': 'id1' + }) + assert_eq(res.status_code, equals=429) + assert_eq(res.text, equals="You are rate limited by Zen.") + + # Now do the same but with a different User in the same time block : + res = requests.get(url, headers={ + 'user': 'id2' + }) + assert_eq(res.status_code, equals=200) + res = requests.get(url, headers={ + 'user': 'id2' + }) + assert_eq(res.status_code, equals=200) + res = requests.get(url, headers={ + 'user': 'id2' + }) + assert_eq(res.status_code, equals=429) + assert_eq(res.text, equals="You are rate limited by Zen.") + res = requests.get(url, headers={ + 'user': 'id2' + }) + assert_eq(res.status_code, equals=429) + assert_eq(res.text, equals="You are rate limited by Zen.") + + # Now wait 5 seconds so your window is over and re-request : + time.sleep(5) + res = requests.get(url, headers={ + 'user': 'id2' + }) + assert_eq(res.status_code, equals=200) + res = requests.get(url, headers={ + 'user': 'id1' + }) + assert_eq(res.status_code, equals=200) + res = requests.get(url, headers={ + 'user': 'id2' + }) + assert_eq(res.status_code, equals=200) + assert_eq(res.status_code, equals=200) + res = requests.get(url, headers={ + 'user': 'id1' + }) + assert_eq(res.status_code, equals=200) + + # Test it prefers user over IP : + res = requests.get(url, headers={ + 'X-Forwarded-For': "192.168.1.2" + }) + assert_eq(res.status_code, equals=200) + res = requests.get(url, headers={ + 'X-Forwarded-For': "192.168.1.2" + }) + assert_eq(res.status_code, equals=200) + res = requests.get(url, headers={ + 'X-Forwarded-For': "192.168.1.2", + 'user': 'id3' + }) + assert_eq(res.status_code, equals=200) + res = requests.get(url, headers={ + 'X-Forwarded-For': "192.168.1.2", + 'user': 'id4' + }) + assert_eq(res.status_code, equals=200) + res = requests.get(url, headers={ + 'X-Forwarded-For': "192.168.1.2", + 'user': 'id5' + }) + assert_eq(res.status_code, equals=200) + res = requests.get(url, headers={ + 'X-Forwarded-For': "192.168.1.2", + 'user': 'id6' + }) + assert_eq(res.status_code, equals=200) \ No newline at end of file From b774a502ebf289d694aa3140eaa34bebf003340e Mon Sep 17 00:00:00 2001 From: BitterPanda63 Date: Fri, 7 Mar 2025 13:36:09 +0100 Subject: [PATCH 02/31] Add django mysql test cases --- end2end/attack_events.json | 24 ++++++++++++++++++++++++ end2end/django_mysql.py | 17 +++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 end2end/attack_events.json create mode 100644 end2end/django_mysql.py diff --git a/end2end/attack_events.json b/end2end/attack_events.json new file mode 100644 index 00000000..c19d12f3 --- /dev/null +++ b/end2end/attack_events.json @@ -0,0 +1,24 @@ +{ + "django_mysql_attack_sql": { + "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" + }, + "django_mysql_attack_shell": { + "blocked": true, + "kind": "shell_injection", + "metadata": { + "command": "ls -la" + }, + "operation": "subprocess.Popen", + "pathToPayload": ".[0]", + "payload": "\"ls -la\"", + "source": "route_params" + } +} diff --git a/end2end/django_mysql.py b/end2end/django_mysql.py new file mode 100644 index 00000000..be3e706f --- /dev/null +++ b/end2end/django_mysql.py @@ -0,0 +1,17 @@ +from __init__ import events +from utils import App, Request + +django_mysql_app = App(8080) + +django_mysql_app.add_payload( + "sql", test_event=events["django_mysql_attack_sql"], + safe_request=Request(route="/app/create", body={'dog_name': 'Bobby'}, data_type="form"), + unsafe_request= Request(route="/app/create", body={'dog_name': 'Dangerous bobby", 1); -- '}, data_type="form") +) +django_mysql_app.add_payload( + "shell", test_event=events["django_mysql_attack_shell"], + safe_request=Request(route="/app/shell/bobby", method="GET"), + unsafe_request=Request(route="/app/shell/ls -la", method="GET") +) + +django_mysql_app.test_all_payloads() From f01e2f87ee7677a60cabf54b83c6e88d7cd3f5ae Mon Sep 17 00:00:00 2001 From: BitterPanda63 Date: Fri, 7 Mar 2025 13:51:28 +0100 Subject: [PATCH 03/31] Add django mysql gunicorn and django postgres gunicorn --- end2end/attack_events.json | 11 ++++ end2end/django_mysql_gunicorn.py | 12 ++++ end2end/django_mysql_test.py | 89 ----------------------------- end2end/django_postgres_gunicorn.py | 12 ++++ 4 files changed, 35 insertions(+), 89 deletions(-) create mode 100644 end2end/django_mysql_gunicorn.py delete mode 100644 end2end/django_mysql_test.py create mode 100644 end2end/django_postgres_gunicorn.py diff --git a/end2end/attack_events.json b/end2end/attack_events.json index c19d12f3..f8128674 100644 --- a/end2end/attack_events.json +++ b/end2end/attack_events.json @@ -20,5 +20,16 @@ "pathToPayload": ".[0]", "payload": "\"ls -la\"", "source": "route_params" + }, + "django_postgres_attack": { + "blocked": true, + "kind": "sql_injection", + "metadata": { + "sql": "INSERT INTO sample_app_Dogs (dog_name, is_admin) VALUES ('Dangerous bobby', TRUE); -- ', FALSE)" + }, + "operation": "psycopg2.Connection.Cursor.execute", + "pathToPayload": ".dog_name", + "payload": "\"Dangerous bobby', TRUE); -- \"", + "source": "body" } } diff --git a/end2end/django_mysql_gunicorn.py b/end2end/django_mysql_gunicorn.py new file mode 100644 index 00000000..1d87efc8 --- /dev/null +++ b/end2end/django_mysql_gunicorn.py @@ -0,0 +1,12 @@ +from __init__ import events +from utils import App, Request + +django_mysql_gunicorn_app = App(8082) + +django_mysql_gunicorn_app.add_payload( + "sql", test_event=events["django_mysql_attack_sql"], + safe_request=Request(route="/app/create", body={'dog_name': 'Bobby'}, data_type="form"), + unsafe_request= Request(route="/app/create", body={'dog_name': 'Dangerous bobby", 1); -- '}, data_type="form") +) + +django_mysql_gunicorn_app.test_all_payloads() diff --git a/end2end/django_mysql_test.py b/end2end/django_mysql_test.py deleted file mode 100644 index 7e499be4..00000000 --- a/end2end/django_mysql_test.py +++ /dev/null @@ -1,89 +0,0 @@ -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 - -# 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}) - 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}) - 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}) - 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_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 - 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]["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 - } - -def test_dangerous_response_without_firewall(): - dog_name = 'Dangerous bobby", 1); -- ' - 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 - 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} - ) diff --git a/end2end/django_postgres_gunicorn.py b/end2end/django_postgres_gunicorn.py new file mode 100644 index 00000000..351ecc2d --- /dev/null +++ b/end2end/django_postgres_gunicorn.py @@ -0,0 +1,12 @@ +from __init__ import events +from utils import App, Request + +django_postgres_gunicorn_app = App(8100) + +django_postgres_gunicorn_app.add_payload( + "sql", test_event=events["django_postgres_attack"], + safe_request=Request(route="/app/create", body={'dog_name': 'Bobby'}, data_type="form"), + unsafe_request= Request(route="/app/create", body={'dog_name': "Dangerous bobby', TRUE); -- "}, data_type="form") +) + +django_postgres_gunicorn_app.test_all_payloads() From 5a84bd51eb8bd16f105fb130b3d989cb0382ffbe Mon Sep 17 00:00:00 2001 From: BitterPanda63 Date: Fri, 7 Mar 2025 13:58:29 +0100 Subject: [PATCH 04/31] Add quart_postgres_attack + starlette/quart e2e --- end2end/attack_events.json | 12 +++++ end2end/quart_postgres_uvicorn.py | 12 +++++ end2end/quart_postgres_uvicorn_test.py | 53 ------------------ end2end/starlette_postgres_uvicorn.py | 12 +++++ end2end/starlette_postgres_uvicorn_test.py | 63 ---------------------- 5 files changed, 36 insertions(+), 116 deletions(-) create mode 100644 end2end/quart_postgres_uvicorn.py delete mode 100644 end2end/quart_postgres_uvicorn_test.py create mode 100644 end2end/starlette_postgres_uvicorn.py delete mode 100644 end2end/starlette_postgres_uvicorn_test.py diff --git a/end2end/attack_events.json b/end2end/attack_events.json index f8128674..17ee7c87 100644 --- a/end2end/attack_events.json +++ b/end2end/attack_events.json @@ -31,5 +31,17 @@ "pathToPayload": ".dog_name", "payload": "\"Dangerous bobby', TRUE); -- \"", "source": "body" + }, + "quart_postgres_attack": { + "blocked": true, + "kind": "sql_injection", + "metadata": { + "sql": "INSERT INTO dogs (dog_name, isAdmin) VALUES ('Dangerous Bobby', TRUE); -- ', FALSE)" + }, + "operation": "asyncpg.connection.Connection.execute", + "pathToPayload": ".dog_name", + "payload": "\"Dangerous Bobby', TRUE); -- \"", + "source": "body", + "user_id": "user123" } } diff --git a/end2end/quart_postgres_uvicorn.py b/end2end/quart_postgres_uvicorn.py new file mode 100644 index 00000000..fbedce3c --- /dev/null +++ b/end2end/quart_postgres_uvicorn.py @@ -0,0 +1,12 @@ +from __init__ import events +from utils import App, Request + +quart_postgres_app = App(8096) + +quart_postgres_app.add_payload( + "sql", test_event=events["quart_postgres_attack"], + safe_request=Request(route="/create", body={'dog_name': 'Bobby'}, data_type="form"), + unsafe_request= Request(route="/create", body={'dog_name': "Dangerous Bobby', TRUE); -- "}, data_type="form") +) + +quart_postgres_app.test_all_payloads() diff --git a/end2end/quart_postgres_uvicorn_test.py b/end2end/quart_postgres_uvicorn_test.py deleted file mode 100644 index ebea4a9a..00000000 --- a/end2end/quart_postgres_uvicorn_test.py +++ /dev/null @@ -1,53 +0,0 @@ -import time -import pytest -import requests -from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type - -# e2e tests for flask_postgres sample app -post_url_fw = "http://localhost:8096/create" -post_url_nofw = "http://localhost:8097/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], None) - -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 == 201 - -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 == 201 - - -def test_dangerous_response_with_firewall(): - 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 - assert attacks[0]["attack"]["kind"] == "sql_injection" - assert attacks[0]["attack"]["metadata"]["sql"] == "INSERT INTO dogs (dog_name, isAdmin) VALUES ('Dangerous Bobby', TRUE); -- ', FALSE)" - assert attacks[0]["attack"]["operation"] == "asyncpg.connection.Connection.execute" - assert attacks[0]["attack"]["pathToPayload"] == '.dog_name' - assert attacks[0]["attack"]["payload"] == "\"Dangerous Bobby', TRUE); -- \"" - assert attacks[0]["attack"]["source"] == "body" - assert attacks[0]["attack"]["user"]["id"] == "user123" - assert attacks[0]["attack"]["user"]["name"] == "John Doe" - - -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 == 201 - diff --git a/end2end/starlette_postgres_uvicorn.py b/end2end/starlette_postgres_uvicorn.py new file mode 100644 index 00000000..2a8f3dc2 --- /dev/null +++ b/end2end/starlette_postgres_uvicorn.py @@ -0,0 +1,12 @@ +from __init__ import events +from utils import App, Request + +starlette_postgres_app = App(8102) + +starlette_postgres_app.add_payload( + "sql", test_event=events["quart_postgres_attack"], + safe_request=Request(route="/create", body={'dog_name': 'Bobby'}, data_type="form"), + unsafe_request= Request(route="/create", body={'dog_name': "Dangerous Bobby', TRUE); -- "}, data_type="form") +) + +starlette_postgres_app.test_all_payloads() diff --git a/end2end/starlette_postgres_uvicorn_test.py b/end2end/starlette_postgres_uvicorn_test.py deleted file mode 100644 index 331bedcd..00000000 --- a/end2end/starlette_postgres_uvicorn_test.py +++ /dev/null @@ -1,63 +0,0 @@ -import time -import pytest -import requests -from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type - -# e2e tests for flask_postgres sample app -post_url_fw = "http://localhost:8102/create" -post_url_nofw = "http://localhost:8103/create" -sync_route_fw = "http://localhost:8102/sync_route" -sync_route_nofw = "http://localhost:8103/sync_route" - -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], None) # Don't assert stack - -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 == 201 - -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 == 201 - - -def test_dangerous_response_with_firewall(): - 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 - assert attacks[0]["attack"]["kind"] == "sql_injection" - assert attacks[0]["attack"]["metadata"]["sql"] == "INSERT INTO dogs (dog_name, isAdmin) VALUES ('Dangerous Bobby', TRUE); -- ', FALSE)" - assert attacks[0]["attack"]["operation"] == "asyncpg.connection.Connection.execute" - assert attacks[0]["attack"]["pathToPayload"] == ".dog_name" - assert attacks[0]["attack"]["payload"] == "\"Dangerous Bobby', TRUE); -- \"" - assert attacks[0]["attack"]["source"] == "body" - assert attacks[0]["attack"]["user"]["id"] == "user123" - assert attacks[0]["attack"]["user"]["name"] == "John Doe" - - -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 == 201 - - -def test_sync_route_with_firewall(): - res = requests.get(sync_route_fw) - assert res.status_code == 200 - -def test_sync_route_without_firewall(): - res = requests.get(sync_route_nofw) - assert res.status_code == 200 From 5e7bd037e97c0403ba79995d4c6fe0b1d3f8c2f7 Mon Sep 17 00:00:00 2001 From: BitterPanda63 Date: Fri, 7 Mar 2025 14:01:18 +0100 Subject: [PATCH 05/31] allow for 201 status codes --- end2end/quart_postgres_uvicorn.py | 2 +- end2end/starlette_postgres_uvicorn.py | 2 +- end2end/utils/__init__.py | 5 +++-- end2end/utils/test_payloads_safe_vs_unsafe.py | 8 ++++---- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/end2end/quart_postgres_uvicorn.py b/end2end/quart_postgres_uvicorn.py index fbedce3c..4abc5fbd 100644 --- a/end2end/quart_postgres_uvicorn.py +++ b/end2end/quart_postgres_uvicorn.py @@ -1,7 +1,7 @@ from __init__ import events from utils import App, Request -quart_postgres_app = App(8096) +quart_postgres_app = App(8096, status_code_valid=201) quart_postgres_app.add_payload( "sql", test_event=events["quart_postgres_attack"], diff --git a/end2end/starlette_postgres_uvicorn.py b/end2end/starlette_postgres_uvicorn.py index 2a8f3dc2..76707c0e 100644 --- a/end2end/starlette_postgres_uvicorn.py +++ b/end2end/starlette_postgres_uvicorn.py @@ -1,7 +1,7 @@ from __init__ import events from utils import App, Request -starlette_postgres_app = App(8102) +starlette_postgres_app = App(8102, status_code_valid=201) starlette_postgres_app.add_payload( "sql", test_event=events["quart_postgres_attack"], diff --git a/end2end/utils/__init__.py b/end2end/utils/__init__.py index 91f834af..72e73b00 100644 --- a/end2end/utils/__init__.py +++ b/end2end/utils/__init__.py @@ -9,13 +9,14 @@ from .test_payloads_safe_vs_unsafe import test_payloads_safe_vs_unsafe class App: - def __init__(self, port): + def __init__(self, port, status_code_valid=200): self.urls = { "enabled": f"http://localhost:{port}", "disabled": f"http://localhost:{port + 1}" } self.payloads = {} self.event_handler = EventHandler() + self.status_code_valid = status_code_valid def add_payload(self,key, safe_request, unsafe_request, test_event=None): self.payloads[key] = { @@ -30,7 +31,7 @@ def test_payload(self, key): payload = self.payloads.get(key) self.event_handler.reset() - test_payloads_safe_vs_unsafe(payload, self.urls) + test_payloads_safe_vs_unsafe(payload, self.urls, status_code1=self.status_code_valid) print("✅ Tested payload: " + key) if payload["test_event"]: diff --git a/end2end/utils/test_payloads_safe_vs_unsafe.py b/end2end/utils/test_payloads_safe_vs_unsafe.py index d88e7dd3..d885efce 100644 --- a/end2end/utils/test_payloads_safe_vs_unsafe.py +++ b/end2end/utils/test_payloads_safe_vs_unsafe.py @@ -1,13 +1,13 @@ from . import assert_eq -def test_payloads_safe_vs_unsafe(payloads, urls): +def test_payloads_safe_vs_unsafe(payloads, urls, status_code1=200): print("Safe req to : (1) " + urls["enabled"] + payloads["safe"].route) - assert_eq(val1=payloads["safe"].execute(urls["enabled"]), equals=200) + assert_eq(val1=payloads["safe"].execute(urls["enabled"]), equals=status_code1) print("Safe req to : (0) " + urls["disabled"] + payloads["safe"].route) - assert_eq(val1=payloads["safe"].execute(urls["disabled"]), equals=200) + assert_eq(val1=payloads["safe"].execute(urls["disabled"]), equals=status_code1) print("Unsafe req to : (1) " + urls["enabled"] + payloads["unsafe"].route) assert_eq(val1=payloads["unsafe"].execute(urls["enabled"]), equals=500) print("Unsafe req to : (0) " + urls["disabled"] + payloads["unsafe"].route) - assert_eq(val1=payloads["unsafe"].execute(urls["disabled"]), equals=200) \ No newline at end of file + assert_eq(val1=payloads["unsafe"].execute(urls["disabled"]), equals=status_code1) From 4890fa870516b687d9c33f5f7e5e02f46c777cdd Mon Sep 17 00:00:00 2001 From: BitterPanda63 Date: Fri, 7 Mar 2025 14:02:37 +0100 Subject: [PATCH 06/31] Fix end2end workflow --- .github/workflows/end2end.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/end2end.yml b/.github/workflows/end2end.yml index 92fabfeb..9d345449 100644 --- a/.github/workflows/end2end.yml +++ b/.github/workflows/end2end.yml @@ -24,17 +24,17 @@ jobs: strategy: matrix: app: - - { 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 } - - { name: flask-mongo, testfile: end2end/flask_mongo_test.py } - - { name: flask-mysql, testfile: end2end/flask_mysql_test.py } - - { name: flask-mysql-uwsgi, testfile: end2end/flask_mysql_uwsgi_test.py } - - { name: flask-postgres, testfile: end2end/flask_postgres_test.py } - - { name: flask-postgres-xml, testfile: end2end/flask_postgres_xml_test.py } - - { name: flask-postgres-xml, testfile: end2end/flask_postgres_xml_lxml_test.py } - - { name: quart-postgres-uvicorn, testfile: end2end/quart_postgres_uvicorn_test.py } - - { name: starlette-postgres-uvicorn, testfile: end2end/starlette_postgres_uvicorn_test.py } + - { name: django-mysql, testfile: end2end/django_mysql.py } + - { name: django-mysql-gunicorn, testfile: end2end/django_mysql_gunicorn.py } + - { name: django-postgres-gunicorn, testfile: end2end/django_postgres_gunicorn.py } + - { name: flask-mongo, testfile: end2end/flask_mongo.py } + - { name: flask-mysql, testfile: end2end/flask_mysql.py } + - { name: flask-mysql-uwsgi, testfile: end2end/flask_mysql_uwsgi.py } + - { name: flask-postgres, testfile: end2end/flask_postgres.py } + - { name: flask-postgres-xml, testfile: end2end/flask_postgres_xml.py } + - { name: flask-postgres-xml, testfile: end2end/flask_postgres_xml_lxml.py } + - { name: quart-postgres-uvicorn, testfile: end2end/quart_postgres_uvicorn.py } + - { name: starlette-postgres-uvicorn, testfile: end2end/starlette_postgres_uvicorn.py } python-version: ["3.10", "3.11", "3.12"] steps: - name: Install packages @@ -67,4 +67,4 @@ jobs: nohup make run > output.log & tail -f output.log & sleep 20 nohup make runZenDisabled & sleep 20 - name: Run end2end tests for application - run: tail -f ./sample-apps/${{ matrix.app.name }}/output.log & poetry run pytest ./${{ matrix.app.testfile }} + run: tail -f ./sample-apps/${{ matrix.app.name }}/output.log & python ./${{ matrix.app.testfile }} From 700d18f05178867c8b5d0e46462daa4ae6644fca Mon Sep 17 00:00:00 2001 From: BitterPanda63 Date: Fri, 7 Mar 2025 14:06:36 +0100 Subject: [PATCH 07/31] Remove copied over space before unsafe_reuqest= --- end2end/django_mysql.py | 2 +- end2end/django_mysql_gunicorn.py | 2 +- end2end/django_postgres_gunicorn.py | 2 +- end2end/quart_postgres_uvicorn.py | 2 +- end2end/starlette_postgres_uvicorn.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/end2end/django_mysql.py b/end2end/django_mysql.py index be3e706f..dbd19b53 100644 --- a/end2end/django_mysql.py +++ b/end2end/django_mysql.py @@ -6,7 +6,7 @@ django_mysql_app.add_payload( "sql", test_event=events["django_mysql_attack_sql"], safe_request=Request(route="/app/create", body={'dog_name': 'Bobby'}, data_type="form"), - unsafe_request= Request(route="/app/create", body={'dog_name': 'Dangerous bobby", 1); -- '}, data_type="form") + unsafe_request=Request(route="/app/create", body={'dog_name': 'Dangerous bobby", 1); -- '}, data_type="form") ) django_mysql_app.add_payload( "shell", test_event=events["django_mysql_attack_shell"], diff --git a/end2end/django_mysql_gunicorn.py b/end2end/django_mysql_gunicorn.py index 1d87efc8..b30987a6 100644 --- a/end2end/django_mysql_gunicorn.py +++ b/end2end/django_mysql_gunicorn.py @@ -6,7 +6,7 @@ django_mysql_gunicorn_app.add_payload( "sql", test_event=events["django_mysql_attack_sql"], safe_request=Request(route="/app/create", body={'dog_name': 'Bobby'}, data_type="form"), - unsafe_request= Request(route="/app/create", body={'dog_name': 'Dangerous bobby", 1); -- '}, data_type="form") + unsafe_request=Request(route="/app/create", body={'dog_name': 'Dangerous bobby", 1); -- '}, data_type="form") ) django_mysql_gunicorn_app.test_all_payloads() diff --git a/end2end/django_postgres_gunicorn.py b/end2end/django_postgres_gunicorn.py index 351ecc2d..cfdbbd15 100644 --- a/end2end/django_postgres_gunicorn.py +++ b/end2end/django_postgres_gunicorn.py @@ -6,7 +6,7 @@ django_postgres_gunicorn_app.add_payload( "sql", test_event=events["django_postgres_attack"], safe_request=Request(route="/app/create", body={'dog_name': 'Bobby'}, data_type="form"), - unsafe_request= Request(route="/app/create", body={'dog_name': "Dangerous bobby', TRUE); -- "}, data_type="form") + unsafe_request=Request(route="/app/create", body={'dog_name': "Dangerous bobby', TRUE); -- "}, data_type="form") ) django_postgres_gunicorn_app.test_all_payloads() diff --git a/end2end/quart_postgres_uvicorn.py b/end2end/quart_postgres_uvicorn.py index 4abc5fbd..3e2e8a44 100644 --- a/end2end/quart_postgres_uvicorn.py +++ b/end2end/quart_postgres_uvicorn.py @@ -6,7 +6,7 @@ quart_postgres_app.add_payload( "sql", test_event=events["quart_postgres_attack"], safe_request=Request(route="/create", body={'dog_name': 'Bobby'}, data_type="form"), - unsafe_request= Request(route="/create", body={'dog_name': "Dangerous Bobby', TRUE); -- "}, data_type="form") + unsafe_request=Request(route="/create", body={'dog_name': "Dangerous Bobby', TRUE); -- "}, data_type="form") ) quart_postgres_app.test_all_payloads() diff --git a/end2end/starlette_postgres_uvicorn.py b/end2end/starlette_postgres_uvicorn.py index 76707c0e..d0804de2 100644 --- a/end2end/starlette_postgres_uvicorn.py +++ b/end2end/starlette_postgres_uvicorn.py @@ -6,7 +6,7 @@ starlette_postgres_app.add_payload( "sql", test_event=events["quart_postgres_attack"], safe_request=Request(route="/create", body={'dog_name': 'Bobby'}, data_type="form"), - unsafe_request= Request(route="/create", body={'dog_name': "Dangerous Bobby', TRUE); -- "}, data_type="form") + unsafe_request=Request(route="/create", body={'dog_name': "Dangerous Bobby', TRUE); -- "}, data_type="form") ) starlette_postgres_app.test_all_payloads() From e6e535efb3b2be8edea11a001caeb5755220abcb Mon Sep 17 00:00:00 2001 From: BitterPanda63 Date: Fri, 7 Mar 2025 14:12:20 +0100 Subject: [PATCH 08/31] Also port the xml/lxml end2end tests --- .github/workflows/end2end.yml | 1 - end2end/attack_events.json | 11 +++++ end2end/flask_postgres_xml.py | 17 ++++++++ end2end/flask_postgres_xml_lxml_test.py | 48 ---------------------- end2end/flask_postgres_xml_test.py | 53 ------------------------- 5 files changed, 28 insertions(+), 102 deletions(-) create mode 100644 end2end/flask_postgres_xml.py delete mode 100644 end2end/flask_postgres_xml_lxml_test.py delete mode 100644 end2end/flask_postgres_xml_test.py diff --git a/.github/workflows/end2end.yml b/.github/workflows/end2end.yml index 9d345449..b45f9fad 100644 --- a/.github/workflows/end2end.yml +++ b/.github/workflows/end2end.yml @@ -32,7 +32,6 @@ jobs: - { name: flask-mysql-uwsgi, testfile: end2end/flask_mysql_uwsgi.py } - { name: flask-postgres, testfile: end2end/flask_postgres.py } - { name: flask-postgres-xml, testfile: end2end/flask_postgres_xml.py } - - { name: flask-postgres-xml, testfile: end2end/flask_postgres_xml_lxml.py } - { name: quart-postgres-uvicorn, testfile: end2end/quart_postgres_uvicorn.py } - { name: starlette-postgres-uvicorn, testfile: end2end/starlette_postgres_uvicorn.py } python-version: ["3.10", "3.11", "3.12"] diff --git a/end2end/attack_events.json b/end2end/attack_events.json index 17ee7c87..b6e6f446 100644 --- a/end2end/attack_events.json +++ b/end2end/attack_events.json @@ -43,5 +43,16 @@ "payload": "\"Dangerous Bobby', TRUE); -- \"", "source": "body", "user_id": "user123" + }, + "flask_xml_attack": { + "blocked": true, + "kind": "sql_injection", + "metadata": { + "sql": "INSERT INTO dogs (dog_name, isAdmin) VALUES ('Malicious dog', TRUE); -- ', FALSE)" + }, + "operation": "psycopg2.Connection.Cursor.execute", + "pathToPayload": ".dog_name.[0]", + "payload": "\"Malicious dog', TRUE); -- \"", + "source": "xml" } } diff --git a/end2end/flask_postgres_xml.py b/end2end/flask_postgres_xml.py new file mode 100644 index 00000000..3e2a3ded --- /dev/null +++ b/end2end/flask_postgres_xml.py @@ -0,0 +1,17 @@ +from __init__ import events +from utils import App, Request + +flask_xml_app = App(8092) + +flask_xml_app.add_payload( + "sql_with_xml", test_event=events["flask_xml_attack"], + safe_request=Request(route="/xml_post", body='', data_type="form"), + unsafe_request=Request(route="/xml_post", body='', data_type="form") +) +flask_xml_app.add_payload( + "sql_with_lxml", test_event=events["flask_xml_attack"], + safe_request=Request(route="/xml_post_lxml", body='', data_type="form"), + unsafe_request=Request(route="/xml_post_lxml", body='', data_type="form") +) + +flask_xml_app.test_all_payloads() diff --git a/end2end/flask_postgres_xml_lxml_test.py b/end2end/flask_postgres_xml_lxml_test.py deleted file mode 100644 index e4455248..00000000 --- a/end2end/flask_postgres_xml_lxml_test.py +++ /dev/null @@ -1,48 +0,0 @@ -import time -import pytest -import requests -from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type - -# e2e tests for flask_postgres sample app -post_url_fw = "http://localhost:8092/xml_post_lxml" -post_url_nofw = "http://localhost:8093/xml_post_lxml" - -def test_safe_response_with_firewall(): - xml_data = '' - res = requests.post(post_url_fw, data=xml_data) - assert res.status_code == 200 - -def test_safe_response_without_firewall(): - xml_data = '' - res = requests.post(post_url_nofw, data=xml_data) - assert res.status_code == 200 - - -def test_dangerous_response_with_firewall(): - xml_data = '' - res = requests.post(post_url_fw, data=xml_data) - 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 dogs (dog_name, isAdmin) VALUES ('Malicious dog', TRUE); -- ', FALSE)"}, - 'operation': "psycopg2.Connection.Cursor.execute", - 'pathToPayload': ".dog_name.[0]", - 'payload': "\"Malicious dog', TRUE); -- \"", - 'source': "xml", - 'user': None - } - -def test_dangerous_response_without_firewall(): - xml_data = '' - res = requests.post(post_url_nofw, data=xml_data) - assert res.status_code == 200 - diff --git a/end2end/flask_postgres_xml_test.py b/end2end/flask_postgres_xml_test.py deleted file mode 100644 index 47b31f21..00000000 --- a/end2end/flask_postgres_xml_test.py +++ /dev/null @@ -1,53 +0,0 @@ -import time -import pytest -import requests -from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type - -# e2e tests for flask_postgres sample app -post_url_fw = "http://localhost:8092/xml_post" -post_url_nofw = "http://localhost:8093/xml_post" - -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], ["flask", "psycopg2-binary", "lxml"]) - -def test_safe_response_with_firewall(): - xml_data = '' - res = requests.post(post_url_fw, data=xml_data) - assert res.status_code == 200 - -def test_safe_response_without_firewall(): - xml_data = '' - res = requests.post(post_url_nofw, data=xml_data) - assert res.status_code == 200 - - -def test_dangerous_response_with_firewall(): - xml_data = '' - res = requests.post(post_url_fw, data=xml_data) - 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 dogs (dog_name, isAdmin) VALUES ('Malicious dog', TRUE); -- ', FALSE)"}, - 'operation': "psycopg2.Connection.Cursor.execute", - 'pathToPayload': ".dog_name.[0]", - 'payload': "\"Malicious dog', TRUE); -- \"", - 'source': "xml", - 'user': None - } -def test_dangerous_response_without_firewall(): - xml_data = '' - res = requests.post(post_url_nofw, data=xml_data) - assert res.status_code == 200 - From dba57be23c688c100ceb6680ee1190aeb57b4f19 Mon Sep 17 00:00:00 2001 From: BitterPanda63 Date: Fri, 7 Mar 2025 14:16:56 +0100 Subject: [PATCH 09/31] Port flask_mysql_uwsgi end2end tests --- end2end/attack_events.json | 11 +++++++ end2end/flask_mysql_uwsgi.py | 12 +++++++ end2end/flask_mysql_uwsgi_test.py | 54 ------------------------------- 3 files changed, 23 insertions(+), 54 deletions(-) create mode 100644 end2end/flask_mysql_uwsgi.py delete mode 100644 end2end/flask_mysql_uwsgi_test.py diff --git a/end2end/attack_events.json b/end2end/attack_events.json index b6e6f446..c47de008 100644 --- a/end2end/attack_events.json +++ b/end2end/attack_events.json @@ -54,5 +54,16 @@ "pathToPayload": ".dog_name.[0]", "payload": "\"Malicious dog', TRUE); -- \"", "source": "xml" + }, + "flask_mysql_attack": { + "blocked": true, + "kind": "sql_injection", + "metadata": { + "sql": "INSERT INTO dogs (dog_name, isAdmin) VALUES (\"Dangerous bobby\", 1); -- \", 0)" + }, + "operation": "pymysql.Cursor.execute", + "pathToPayload": ".dog_name", + "payload": "\"Dangerous bobby\\\\\", 1); -- \"", + "source": "body" } } diff --git a/end2end/flask_mysql_uwsgi.py b/end2end/flask_mysql_uwsgi.py new file mode 100644 index 00000000..f940ee95 --- /dev/null +++ b/end2end/flask_mysql_uwsgi.py @@ -0,0 +1,12 @@ +from __init__ import events +from utils import App, Request + +flask_mysql_uwsgi_app = App(8088) + +flask_mysql_uwsgi_app.add_payload( + "sql", test_event=events["flask_mysql_attack"], + safe_request=Request(route="/create", body={'dog_name': 'Bobby'}, data_type="form"), + unsafe_request=Request(route="/create", body={'dog_name': 'Dangerous bobby", 1); -- '}, data_type="form") +) + +flask_mysql_uwsgi_app.test_all_payloads() diff --git a/end2end/flask_mysql_uwsgi_test.py b/end2end/flask_mysql_uwsgi_test.py deleted file mode 100644 index a1917a79..00000000 --- a/end2end/flask_mysql_uwsgi_test.py +++ /dev/null @@ -1,54 +0,0 @@ -import time -import pytest -import requests -from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type - -# e2e tests for flask_mysql_uwsgi sample app -post_url_fw = "http://localhost:8088/create" -post_url_nofw = "http://localhost:8089/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], ["flask", "pymysql", "uwsgi"]) - -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(): - dog_name = 'Dangerous bobby", 1); -- ' - 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': {'sql': 'INSERT INTO dogs (dog_name, isAdmin) VALUES ("Dangerous bobby", 1); -- ", 0)'}, - 'operation': 'pymysql.Cursor.execute', - 'pathToPayload': '.dog_name', - 'payload': '"Dangerous bobby\\", 1); -- "', - 'source': "body", - 'user': None - } - -def test_dangerous_response_without_firewall(): - dog_name = 'Dangerous bobby", 1); -- ' - res = requests.post(post_url_nofw, data={'dog_name': dog_name}) - assert res.status_code == 200 - From 35d9d1db49ed7574f3ddae8788f6caea7e354d0a Mon Sep 17 00:00:00 2001 From: BitterPanda63 Date: Fri, 7 Mar 2025 15:31:07 +0100 Subject: [PATCH 10/31] Remopve tests that are already ported --- end2end/django_mysql_gunicorn_test.py | 54 ------------------------ end2end/django_postgres_gunicorn_test.py | 54 ------------------------ 2 files changed, 108 deletions(-) delete mode 100644 end2end/django_mysql_gunicorn_test.py delete mode 100644 end2end/django_postgres_gunicorn_test.py diff --git a/end2end/django_mysql_gunicorn_test.py b/end2end/django_mysql_gunicorn_test.py deleted file mode 100644 index 4637a343..00000000 --- a/end2end/django_mysql_gunicorn_test.py +++ /dev/null @@ -1,54 +0,0 @@ -import pytest -import requests -import time -from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type - -# e2e tests for django_mysql_gunicorn sample app -post_url_fw = "http://localhost:8082/app/create/" -post_url_nofw = "http://localhost:8083/app/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"]) - -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(): - dog_name = 'Dangerous bobby", 1); -- ' - 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': {'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_without_firewall(): - dog_name = 'Dangerous bobby", 1); -- ' - res = requests.post(post_url_nofw, data={'dog_name': dog_name}) - assert res.status_code == 200 - diff --git a/end2end/django_postgres_gunicorn_test.py b/end2end/django_postgres_gunicorn_test.py deleted file mode 100644 index 4815558d..00000000 --- a/end2end/django_postgres_gunicorn_test.py +++ /dev/null @@ -1,54 +0,0 @@ -import pytest -import requests -import time -from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type - -# e2e tests for django_postgres_gunicorn sample app -post_url_fw = "http://localhost:8100/app/create" -post_url_nofw = "http://localhost:8101/app/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", "psycopg2-binary"]) - -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(): - 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': {'sql': "INSERT INTO sample_app_Dogs (dog_name, is_admin) VALUES ('Dangerous bobby', TRUE); -- ', FALSE)"}, - 'operation': "psycopg2.Connection.Cursor.execute", - 'pathToPayload': '.dog_name', - '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 cb4275a19e7fd70cb64523f7c9ddd5fbe99e50ac Mon Sep 17 00:00:00 2001 From: BitterPanda63 Date: Fri, 7 Mar 2025 15:31:29 +0100 Subject: [PATCH 11/31] Add trailing slashes for django_mysql_gunicorn --- end2end/django_mysql_gunicorn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/end2end/django_mysql_gunicorn.py b/end2end/django_mysql_gunicorn.py index b30987a6..29017955 100644 --- a/end2end/django_mysql_gunicorn.py +++ b/end2end/django_mysql_gunicorn.py @@ -5,8 +5,8 @@ django_mysql_gunicorn_app.add_payload( "sql", test_event=events["django_mysql_attack_sql"], - safe_request=Request(route="/app/create", body={'dog_name': 'Bobby'}, data_type="form"), - unsafe_request=Request(route="/app/create", body={'dog_name': 'Dangerous bobby", 1); -- '}, data_type="form") + safe_request=Request(route="/app/create/", body={'dog_name': 'Bobby'}, data_type="form"), + unsafe_request=Request(route="/app/create/", body={'dog_name': 'Dangerous bobby", 1); -- '}, data_type="form") ) django_mysql_gunicorn_app.test_all_payloads() From 8f24957d177685dbbc511762defff53f046a4622 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Fri, 7 Mar 2025 16:23:40 +0100 Subject: [PATCH 12/31] remove \\ from flask mysql (failed copy) --- end2end/attack_events.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/end2end/attack_events.json b/end2end/attack_events.json index c47de008..8513d50d 100644 --- a/end2end/attack_events.json +++ b/end2end/attack_events.json @@ -63,7 +63,7 @@ }, "operation": "pymysql.Cursor.execute", "pathToPayload": ".dog_name", - "payload": "\"Dangerous bobby\\\\\", 1); -- \"", + "payload": "\"Dangerous bobby\\\", 1); -- \"", "source": "body" } } From ce469c67b4fef0360d1636af4ac03db17bafe252 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Fri, 7 Mar 2025 16:43:38 +0100 Subject: [PATCH 13/31] flask_mongo port test cases to new system --- end2end/attack_events.json | 11 ++++ end2end/flask_mongo.py | 24 +++++++ end2end/flask_mongo_test.py | 128 ------------------------------------ 3 files changed, 35 insertions(+), 128 deletions(-) create mode 100644 end2end/flask_mongo.py delete mode 100644 end2end/flask_mongo_test.py diff --git a/end2end/attack_events.json b/end2end/attack_events.json index 8513d50d..20155e12 100644 --- a/end2end/attack_events.json +++ b/end2end/attack_events.json @@ -65,5 +65,16 @@ "pathToPayload": ".dog_name", "payload": "\"Dangerous bobby\\\", 1); -- \"", "source": "body" + }, + "flask_mongo_attack": { + "blocked": true, + "kind": "nosql_injection", + "metadata": { + "filter": "{\"dog_name\": \"bobby_tables\", \"pswd\": {\"$ne\": \"\"}}" + }, + "operation": "pymongo.collection.Collection.find", + "pathToPayload": ".pswd", + "payload": "{\"$ne\": \"\"}", + "source": "body" } } diff --git a/end2end/flask_mongo.py b/end2end/flask_mongo.py new file mode 100644 index 00000000..2c3dbe26 --- /dev/null +++ b/end2end/flask_mongo.py @@ -0,0 +1,24 @@ +from __init__ import events +from utils import App, Request + +flask_mongo_app = App(8094) + +# Create dog : +status_code_create = Request(route="/create", body={ + "dog_name": "bobby_tables", "pswd": "bobby123" +}, data_type="form").execute(flask_mongo_app.urls["enabled"]) +assert status_code_create == 200 + +# Payloads : +flask_mongo_app.add_payload( + "nosql", test_event=events["flask_mongo_attack"], + safe_request=Request("/auth", body={"dog_name": "bobby_tables", "pswd": "bobby123"}), + unsafe_request=Request("/auth", body={"dog_name": "bobby_tables", "pswd": { "$ne": ""}}) +) +flask_mongo_app.add_payload( + "nosql_force", test_event=events["flask_mongo_attack"], + safe_request=Request("/auth_force", body={"dog_name": "bobby_tables", "pswd": "bobby123"}), + unsafe_request=Request("/auth_force", body={"dog_name": "bobby_tables", "pswd": { "$ne": ""}}) +) + +flask_mongo_app.test_all_payloads() diff --git a/end2end/flask_mongo_test.py b/end2end/flask_mongo_test.py deleted file mode 100644 index bb32589c..00000000 --- a/end2end/flask_mongo_test.py +++ /dev/null @@ -1,128 +0,0 @@ -import json -import time -import pytest -import requests -from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type - -# e2e tests for flask_mysql sample app -post_url_fw = "http://localhost:8094/create" -post_url_nofw = "http://localhost:8095/create" - -post_json_url_fw = "http://localhost:8094/auth" -post_json_url_nofw = "http://localhost:8095/auth" - - -# Create dogs: -def test_create_dog_fw(): - dog_name = "bobby_tables" - pswd = "bobby123" - res = requests.post(post_url_fw, data={'dog_name': dog_name, 'pswd': pswd}) - print(res.text) - assert "created successfully" in res.text - assert res.status_code == 200 -def test_create_dog_no_fw(): - dog_name = "bobby_tables2" - pswd = "bobby123" - res = requests.post(post_url_nofw, data={'dog_name': dog_name, 'pswd': pswd}) - print(res.text) - assert "created successfully" in res.text - assert res.status_code == 200 - -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], ["flask", "pymongo"]) - -# Auth dogs with right password: -def test_safe_auth_fw(): - dog_name = "bobby_tables" - pswd = "bobby123" - res = requests.post(post_json_url_fw, json={'dog_name': dog_name, "pswd": pswd}) - assert res.ok - assert res.text == "Dog with name bobby_tables authenticated successfully" - assert res.status_code == 200 -def test_safe_auth_nofw(): - dog_name = "bobby_tables" - pswd = "bobby123" - res = requests.post(post_json_url_nofw, json={'dog_name': dog_name, "pswd": pswd}) - assert res.ok - assert res.text == "Dog with name bobby_tables authenticated successfully" - assert res.status_code == 200 - -# Auth dogs with wrong password: -def test_safe_auth_wrong_pswd_fw(): - dog_name = "bobby_tables" - pswd = "WrongPassword" - res = requests.post(post_json_url_fw, json={'dog_name': dog_name, "pswd": pswd}) - assert res.ok - assert res.text == "Auth failed" - assert res.status_code == 200 -def test_safe_auth_wrong_pswd_nofw(): - dog_name = "bobby_tables" - pswd = "WrongPassword" - res = requests.post(post_json_url_nofw, json={'dog_name': dog_name, "pswd": pswd}) - assert res.ok - assert res.text == "Auth failed" - assert res.status_code == 200 - -# Test NoSQL injection: -def test_dangerous_auth_fw(): - dog_name = "bobby_tables" - pswd = { "$ne": ""} - res = requests.post(post_json_url_fw, json={'dog_name': dog_name, "pswd": pswd}) - - assert not res.ok - 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": "nosql_injection", - 'metadata': {'filter': '{"dog_name": "bobby_tables", "pswd": {"$ne": ""}}'}, - 'operation': "pymongo.collection.Collection.find", - 'pathToPayload': ".pswd", - 'payload': '{"$ne": ""}', - 'source': "body", - 'user': None - } - -def test_dangerous_auth_nofw(): - dog_name = "bobby_tables" - pswd = { "$ne": ""} - res = requests.post(post_json_url_nofw, json={'dog_name': dog_name, "pswd": pswd}) - assert res.ok - assert res.text == "Dog with name bobby_tables authenticated successfully" - assert res.status_code == 200 - - -def test_dangerous_auth_fw_force(): - dog_name = "bobby_tables" - pswd = {"$ne": ""} - json_data = json.dumps({'dog_name': dog_name, "pswd": pswd}) - res = requests.post(post_json_url_fw + "_force", data=json_data) - - assert not res.ok - 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) == 2 - del attacks[0]["attack"]["stack"] - assert attacks[0]["attack"] == { - "blocked": True, - "kind": "nosql_injection", - 'metadata': {'filter': '{"dog_name": "bobby_tables", "pswd": {"$ne": ""}}'}, - 'operation': "pymongo.collection.Collection.find", - 'pathToPayload': ".pswd", - 'payload': '{"$ne": ""}', - 'source': "body", - 'user': None - } From d9bc50552050c741847ee1ff5f15b0dc2eb4cd66 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Fri, 7 Mar 2025 16:58:41 +0100 Subject: [PATCH 14/31] Convert flask_postgres to the new testing framework --- end2end/attack_events.json | 22 +++++++ end2end/flask_postgres.py | 17 ++++++ end2end/flask_postgres_test.py | 104 --------------------------------- end2end/utils/request.py | 9 +-- 4 files changed, 44 insertions(+), 108 deletions(-) create mode 100644 end2end/flask_postgres.py delete mode 100644 end2end/flask_postgres_test.py diff --git a/end2end/attack_events.json b/end2end/attack_events.json index 20155e12..ea5e0ecc 100644 --- a/end2end/attack_events.json +++ b/end2end/attack_events.json @@ -76,5 +76,27 @@ "pathToPayload": ".pswd", "payload": "{\"$ne\": \"\"}", "source": "body" + }, + "flask_postgres_attack_body": { + "blocked": true, + "kind": "sql_injection", + "metadata": { + "sql": "INSERT INTO dogs (dog_name, isAdmin) VALUES ('Dangerous Bobby', TRUE); -- ', FALSE)" + }, + "operation": "psycopg2.Connection.Cursor.execute", + "pathToPayload": ".dog_name", + "payload": "\"Dangerous Bobby', TRUE); -- \"", + "source": "body" + }, + "flask_postgres_attack_cookie": { + "blocked": true, + "kind": "sql_injection", + "metadata": { + "sql": "INSERT INTO dogs (dog_name, isAdmin) VALUES ('Bobby', TRUE) --', FALSE)" + }, + "operation": "psycopg2.Connection.Cursor.execute", + "pathToPayload": ".dog_name", + "payload": "\"Bobby', TRUE) --\"", + "source": "cookies" } } diff --git a/end2end/flask_postgres.py b/end2end/flask_postgres.py new file mode 100644 index 00000000..7cd46d60 --- /dev/null +++ b/end2end/flask_postgres.py @@ -0,0 +1,17 @@ +from __init__ import events +from utils import App, Request + +flask_postgres_app = App(8090) + +flask_postgres_app.add_payload( + "sql", test_event=events["flask_postgres_attack_body"], + safe_request=Request("/create", body={"dog_name": "Bobby Tables"}, data_type="form"), + unsafe_request=Request("/create", body={"dog_name": "Dangerous Bobby', TRUE); -- "}, data_type="form") +) +flask_postgres_app.add_payload( + "sql_cookie", test_event=["flask_postgres_attack_cookie"], + safe_request=Request("/create_with_cookie", method="GET", cookies={"dog_name": "Bobby Tables"}), + unsafe_request=Request("/create_with_cookie", method="GET", cookies={"dog_name": "Bobby', TRUE) -- "}) +) + +flask_postgres_app.test_all_payloads() diff --git a/end2end/flask_postgres_test.py b/end2end/flask_postgres_test.py deleted file mode 100644 index 16be5222..00000000 --- a/end2end/flask_postgres_test.py +++ /dev/null @@ -1,104 +0,0 @@ -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 - -# e2e tests for flask_postgres sample app -post_url_fw = "http://localhost:8090/create" -post_url_nofw = "http://localhost:8091/create" -get_url_cookie_fw = "http://localhost:8090/create_with_cookie" -get_url_cookie_nofw = "http://localhost:8091/create_with_cookie" - -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], ["flask", "psycopg2-binary"]) - -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(): - dog_name = "Dangerous Bobby', TRUE); -- " - res = requests.post(post_url_fw, data={'dog_name': dog_name}) - assert res.status_code == 500 - -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 - - -def test_safe_cookie_creation_with_firewall(): - cookies = { - "dog_name": "Bobby Tables", - "corrupt_data": ";;;;;;;;;;;;;" - } - res = requests.get(get_url_cookie_fw, cookies=cookies) - assert res.status_code == 200 - -def test_safe_cookie_creation_without_firewall(): - cookies = { - "dog_name": "Bobby Tables", - "corrupt_data": ";;;;;;;;;;;;;" - - } - res = requests.get(get_url_cookie_nofw, cookies=cookies) - assert res.status_code == 200 - - -def test_dangerous_cookie_creation_with_firewall(): - cookies = { - "dog_name": "Bobby', TRUE) -- ", - "corrupt_data": ";;;;;;;;;;;;;" - } - res = requests.get(get_url_cookie_fw, cookies=cookies) - assert res.status_code == 500 - -def test_dangerous_cookie_creation_without_firewall(): - cookies = { - "dog_name": "Bobby', TRUE) -- ", - "corrupt_data": ";;;;;;;;;;;;;" - } - res = requests.get(get_url_cookie_nofw, cookies=cookies) - assert res.status_code == 200 - -def test_attacks_detected(): - 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]["attack"]["stack"] - del attacks[1]["attack"]["stack"] - - assert attacks[0]["attack"] == { - "blocked": True, - "kind": "sql_injection", - 'metadata': {'sql': "INSERT INTO dogs (dog_name, isAdmin) VALUES ('Dangerous Bobby', TRUE); -- ', FALSE)"}, - 'operation': "psycopg2.Connection.Cursor.execute", - 'pathToPayload': '.dog_name', - 'payload': '"Dangerous Bobby\', TRUE); -- "', - 'source': "body", - 'user': None - } - assert attacks[1]["attack"] == { - "blocked": True, - "kind": "sql_injection", - 'metadata': {'sql': "INSERT INTO dogs (dog_name, isAdmin) VALUES ('Bobby', TRUE) --', FALSE)"}, - 'operation': "psycopg2.Connection.Cursor.execute", - 'pathToPayload': '.dog_name', - 'payload': "\"Bobby', TRUE) --\"", - 'source': "cookies", - 'user': None - } diff --git a/end2end/utils/request.py b/end2end/utils/request.py index 76a24562..f35d7a84 100644 --- a/end2end/utils/request.py +++ b/end2end/utils/request.py @@ -1,12 +1,13 @@ import requests class Request: - def __init__(self, route, method='POST', headers=None, data_type='json', body=None): + def __init__(self, route, method='POST', headers=None, data_type='json', body=None, cookies={}): self.method = method self.route = route self.headers = headers if headers is not None else {} self.data_type = data_type # 'json' or 'form' self.body = body + self.cookies = cookies def execute(self, base_url): """Execute the request and return the status code.""" @@ -14,13 +15,13 @@ def execute(self, base_url): if self.method.upper() == 'POST': if self.data_type == 'json': - response = requests.post(url, json=self.body, headers=self.headers) + response = requests.post(url, json=self.body, headers=self.headers, cookies=self.cookies) elif self.data_type == 'form': - response = requests.post(url, data=self.body, headers=self.headers) + response = requests.post(url, data=self.body, headers=self.headers, cookies=self.cookies) else: raise ValueError("Unsupported data type. Use 'json' or 'form'.") elif self.method.upper() == 'GET': - response = requests.get(url, headers=self.headers) + response = requests.get(url, headers=self.headers, cookies=self.cookies) else: raise ValueError("Unsupported HTTP method. Use 'GET' or 'POST'.") From 9b73bb61e2c923e3f941caa840e95511e3ee7130 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Sat, 8 Mar 2025 23:32:24 +0000 Subject: [PATCH 15/31] Update flask_postgres.py --- end2end/flask_postgres.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/end2end/flask_postgres.py b/end2end/flask_postgres.py index 7cd46d60..48989c09 100644 --- a/end2end/flask_postgres.py +++ b/end2end/flask_postgres.py @@ -9,7 +9,7 @@ unsafe_request=Request("/create", body={"dog_name": "Dangerous Bobby', TRUE); -- "}, data_type="form") ) flask_postgres_app.add_payload( - "sql_cookie", test_event=["flask_postgres_attack_cookie"], + "sql_cookie", test_event=events["flask_postgres_attack_cookie"], safe_request=Request("/create_with_cookie", method="GET", cookies={"dog_name": "Bobby Tables"}), unsafe_request=Request("/create_with_cookie", method="GET", cookies={"dog_name": "Bobby', TRUE) -- "}) ) From 0d5348599971d303ce2f1723b53ebbc9a830f79f Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 10 Mar 2025 09:19:57 +0100 Subject: [PATCH 16/31] Create flask_mysql.py file that also tests IP Blocking and rate-limiting --- end2end/attack_events.json | 24 ++++++++ end2end/flask_mysql.py | 19 ++++++ end2end/flask_mysql_test.py | 111 ------------------------------------ end2end/utils/__init__.py | 4 +- 4 files changed, 45 insertions(+), 113 deletions(-) create mode 100644 end2end/flask_mysql.py delete mode 100644 end2end/flask_mysql_test.py diff --git a/end2end/attack_events.json b/end2end/attack_events.json index ea5e0ecc..44afadab 100644 --- a/end2end/attack_events.json +++ b/end2end/attack_events.json @@ -98,5 +98,29 @@ "pathToPayload": ".dog_name", "payload": "\"Bobby', TRUE) --\"", "source": "cookies" + }, + "flask_mysql_attack_sql": { + "blocked": true, + "kind": "sql_injection", + "metadata": { + "sql": "INSERT INTO dogs (dog_name, isAdmin) VALUES (\"Dangerous bobby\", 1); -- \", 0)" + }, + "operation": "pymysql.Cursor.execute", + "pathToPayload": ".dog_name", + "payload": "\"Dangerous bobby\\\", 1); -- \"", + "source": "body", + "user_id": "123" + }, + "flask_mysql_attack_shell": { + "blocked": true, + "kind": "shell_injection", + "metadata": { + "command": "ls -la" + }, + "operation": "subprocess.Popen", + "pathToPayload": ".command", + "payload": "\"ls -la\"", + "source": "route_params", + "user_id": "123" } } diff --git a/end2end/flask_mysql.py b/end2end/flask_mysql.py new file mode 100644 index 00000000..f3c82b93 --- /dev/null +++ b/end2end/flask_mysql.py @@ -0,0 +1,19 @@ +from __init__ import events +from utils import App, Request + +flask_mysql_app = App(8086) + +flask_mysql_app.add_payload( + "sql", test_event=events["flask_mysql_attack_sql"], + safe_request=Request("/create", body={"dog_name": "Bobby"}, data_type="form"), + unsafe_request=Request("/create", body={"dog_name": "Bobby"}, data_type="form") +) +flask_mysql_app.add_payload( + "shell", test_event=events["flask_mysql_attack_shell"], + safe_request=Request(route="/shell/bobby", method="GET"), + unsafe_request=Request(route="/shell/ls -la", method="GET") +) + +flask_mysql_app.test_all_payloads() +flask_mysql_app.test_rate_limiting() +flask_mysql_app.test_blocking() diff --git a/end2end/flask_mysql_test.py b/end2end/flask_mysql_test.py deleted file mode 100644 index 2365521e..00000000 --- a/end2end/flask_mysql_test.py +++ /dev/null @@ -1,111 +0,0 @@ -import pytest -import requests -import time -from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type -# e2e tests for flask_mysql sample app -base_url_fw = "http://localhost:8086" -base_url_nofw = "http://localhost:8087" - -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], ["flask", "pymysql"]) - -def test_safe_response_with_firewall(): - dog_name = "Bobby Tables" - 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}) - assert res.status_code == 200 - - -def test_dangerous_response_with_firewall(): - events = fetch_events_from_mock("http://localhost:5000") - assert len(filter_on_event_type(events, "detected_attack")) == 0 - dog_name = 'Dangerous bobby", 1); -- ' - 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 - assert attacks[0]["attack"]["kind"] == "sql_injection" - assert attacks[0]["attack"]["metadata"]["sql"] == 'INSERT INTO dogs (dog_name, isAdmin) VALUES ("Dangerous bobby", 1); -- ", 0)' - assert attacks[0]["attack"]["operation"] == 'pymysql.Cursor.execute' - assert attacks[0]["attack"]["pathToPayload"] == '.dog_name' - assert attacks[0]["attack"]["payload"] == '"Dangerous bobby\\", 1); -- "' - assert attacks[0]["attack"]["source"] == "body" - assert attacks[0]["attack"]["user"]["id"] == "123" - assert attacks[0]["attack"]["user"]["name"] == "John Doe" - - -def test_dangerous_response_with_firewall_route_params(): - events = fetch_events_from_mock("http://localhost:5000") - assert len(filter_on_event_type(events, "detected_attack")) == 1 - res = requests.get(base_url_fw + "/shell/ls -la") - 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) == 2 - del attacks[0] - assert attacks[0]["attack"]["blocked"] == True - assert attacks[0]["attack"]["kind"] == "shell_injection" - assert attacks[0]["attack"]['metadata']['command'] == 'ls -la' - assert attacks[0]["attack"]["operation"] == 'subprocess.Popen' - assert attacks[0]["attack"]["pathToPayload"] == '.command' - assert attacks[0]["attack"]["payload"] == '"ls -la"' - assert attacks[0]["attack"]["source"] == "route_params" - assert attacks[0]["attack"]["user"]["id"] == "123" - assert attacks[0]["attack"]["user"]["name"] == "John Doe" - - -def test_dangerous_response_without_firewall(): - dog_name = 'Dangerous bobby", 1); -- ' - res = requests.post(base_url_nofw + "/create", data={'dog_name': dog_name}) - assert res.status_code == 200 - -def test_ratelimiting_1_route(): - # First request : - res = requests.get(base_url_fw + "/test_ratelimiting_1") - assert res.status_code == 200 - # Second request : - res = requests.get(base_url_fw + "/test_ratelimiting_1") - assert res.status_code == 200 - # Third request : - res = requests.get(base_url_fw + "/test_ratelimiting_1") - assert res.status_code == 429 - # Fourth request : - res = requests.get(base_url_fw + "/test_ratelimiting_1") - assert res.status_code == 429 - - time.sleep(5) # Wait until window expires - - # Fifth request : - res = requests.get(base_url_fw + "/test_ratelimiting_1") - assert res.status_code == 200 - - -def test_set_ip_forwarded_for(): - # IP allowed : - res = requests.get(base_url_fw + "/", headers={ - "X-Forwarded-For": "1.1.1.1" - }) - assert res.status_code == 200 - # IP Geo-blocked : - res = requests.get(base_url_fw + "/", headers={ - "X-Forwarded-For": "1.2.3.4" - }) - assert res.status_code == 403 - assert res.text == "Your IP address is blocked due to geo restrictions (Your IP: 1.2.3.4)" diff --git a/end2end/utils/__init__.py b/end2end/utils/__init__.py index 72e73b00..92532ecd 100644 --- a/end2end/utils/__init__.py +++ b/end2end/utils/__init__.py @@ -50,8 +50,8 @@ def test_all_payloads(self): self.test_payload(key) def test_blocking(self): - test_bot_blocking(self.urls["enabled"]) - print("✅ Tested bot blocking") + #test_bot_blocking(self.urls["enabled"]) + #print("✅ Tested bot blocking") test_ip_blocking(self.urls["enabled"]) print("✅ Tested IP Blocking") From 8c3d095bedb36d62dbf92c7256e2c8fd773c961c Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 10 Mar 2025 09:22:52 +0100 Subject: [PATCH 17/31] Allow setting of user through user header --- end2end/attack_events.json | 2 +- end2end/flask_mysql.py | 2 +- sample-apps/flask-mysql/app.py | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/end2end/attack_events.json b/end2end/attack_events.json index 44afadab..abcd6347 100644 --- a/end2end/attack_events.json +++ b/end2end/attack_events.json @@ -121,6 +121,6 @@ "pathToPayload": ".command", "payload": "\"ls -la\"", "source": "route_params", - "user_id": "123" + "user_id": "456" } } diff --git a/end2end/flask_mysql.py b/end2end/flask_mysql.py index f3c82b93..3eca25c1 100644 --- a/end2end/flask_mysql.py +++ b/end2end/flask_mysql.py @@ -11,7 +11,7 @@ flask_mysql_app.add_payload( "shell", test_event=events["flask_mysql_attack_shell"], safe_request=Request(route="/shell/bobby", method="GET"), - unsafe_request=Request(route="/shell/ls -la", method="GET") + unsafe_request=Request(route="/shell/ls -la", method="GET", headers={"user": "456"}) ) flask_mysql_app.test_all_payloads() diff --git a/sample-apps/flask-mysql/app.py b/sample-apps/flask-mysql/app.py index e96f47ae..6f1dd309 100644 --- a/sample-apps/flask-mysql/app.py +++ b/sample-apps/flask-mysql/app.py @@ -13,6 +13,7 @@ import subprocess from flask import Flask, render_template, request from flaskext.mysql import MySQL +from werkzeug.wrappers import Request import requests import subprocess @@ -25,7 +26,11 @@ class SetUserMiddleware: def __init__(self, app): self.app = app def __call__(self, environ, start_response): - aikido_zen.set_user({"id": "123", "name": "John Doe"}) + req = Request(environ, shallow=True) + if req.headers.get("USER"): + aikido_zen.set_user({"id": req.headers.get("USER"), "name": "John Doe"}) + else: + aikido_zen.set_user({"id": "123", "name": "John Doe"}) return self.app(environ, start_response) app.wsgi_app = AikidoFlaskMiddleware(app.wsgi_app) app.wsgi_app = SetUserMiddleware(app.wsgi_app) From 5cba5ec9050cc54ee126fead6f383c4b704733df Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 10 Mar 2025 09:29:48 +0100 Subject: [PATCH 18/31] Change the "dangerous" payload for flask mysql --- end2end/flask_mysql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/end2end/flask_mysql.py b/end2end/flask_mysql.py index 3eca25c1..f6f78f40 100644 --- a/end2end/flask_mysql.py +++ b/end2end/flask_mysql.py @@ -6,7 +6,7 @@ flask_mysql_app.add_payload( "sql", test_event=events["flask_mysql_attack_sql"], safe_request=Request("/create", body={"dog_name": "Bobby"}, data_type="form"), - unsafe_request=Request("/create", body={"dog_name": "Bobby"}, data_type="form") + unsafe_request=Request("/create", body={"dog_name": "Dangerous bobby\", 1); -- "}, data_type="form") ) flask_mysql_app.add_payload( "shell", test_event=events["flask_mysql_attack_shell"], From 1143a5937a47f247d9cfa56fe2ec0d07c3e06ff2 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 10 Mar 2025 09:35:10 +0100 Subject: [PATCH 19/31] Only set user id when the user header is present --- end2end/attack_events.json | 3 +-- sample-apps/flask-mysql/app.py | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/end2end/attack_events.json b/end2end/attack_events.json index abcd6347..bf0ad426 100644 --- a/end2end/attack_events.json +++ b/end2end/attack_events.json @@ -108,8 +108,7 @@ "operation": "pymysql.Cursor.execute", "pathToPayload": ".dog_name", "payload": "\"Dangerous bobby\\\", 1); -- \"", - "source": "body", - "user_id": "123" + "source": "body" }, "flask_mysql_attack_shell": { "blocked": true, diff --git a/sample-apps/flask-mysql/app.py b/sample-apps/flask-mysql/app.py index 6f1dd309..21c0d43c 100644 --- a/sample-apps/flask-mysql/app.py +++ b/sample-apps/flask-mysql/app.py @@ -29,8 +29,6 @@ def __call__(self, environ, start_response): req = Request(environ, shallow=True) if req.headers.get("USER"): aikido_zen.set_user({"id": req.headers.get("USER"), "name": "John Doe"}) - else: - aikido_zen.set_user({"id": "123", "name": "John Doe"}) return self.app(environ, start_response) app.wsgi_app = AikidoFlaskMiddleware(app.wsgi_app) app.wsgi_app = SetUserMiddleware(app.wsgi_app) From 2a157ab9aa58c5ea970f2fb5b6b34e653debf6a8 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 10 Mar 2025 09:44:04 +0100 Subject: [PATCH 20/31] Change block message to the one used by python --- end2end/utils/test_ip_blocking.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/end2end/utils/test_ip_blocking.py b/end2end/utils/test_ip_blocking.py index 417a3c7e..0b65cf8e 100644 --- a/end2end/utils/test_ip_blocking.py +++ b/end2end/utils/test_ip_blocking.py @@ -13,14 +13,14 @@ def test_ip_blocking(url): 'X-Forwarded-For': "1.2.3.4" }) assert_eq(res.status_code, equals=403) - assert_eq(res.text, equals="Your IP address is not allowed to access this resource. (Your IP: 1.2.3.4)") + assert_eq(res.text, equals="Your IP address is blocked due to geo restrictions (Your IP: 1.2.3.4)") # More complex X-Forwarded-For : res = requests.get(url, headers={ 'X-Forwarded-For': "invalid.ip.here.now, 1.2.3.4 " }) assert_eq(res.status_code, equals=403) - assert_eq(res.text, equals="Your IP address is not allowed to access this resource. (Your IP: 1.2.3.4)") + assert_eq(res.text, equals="Your IP address is blocked due to geo restrictions (Your IP: 1.2.3.4)") # More complex but safe X-Forwarded-For : res = requests.get(url, headers={ @@ -38,4 +38,4 @@ def test_ip_blocking(url): res = requests.get(url, headers={ 'X-Forwarded-For': "" }) - assert_eq(res.status_code, equals=200) \ No newline at end of file + assert_eq(res.status_code, equals=200) From 681d9d7a42814bbd14fa729e43a3548e4a8c2782 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 10 Mar 2025 09:50:56 +0100 Subject: [PATCH 21/31] More testing in quart e2e tests --- end2end/quart_postgres_uvicorn.py | 2 ++ sample-apps/quart-postgres-uvicorn/app.py | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/end2end/quart_postgres_uvicorn.py b/end2end/quart_postgres_uvicorn.py index 3e2e8a44..a099f5d9 100644 --- a/end2end/quart_postgres_uvicorn.py +++ b/end2end/quart_postgres_uvicorn.py @@ -10,3 +10,5 @@ ) quart_postgres_app.test_all_payloads() +quart_postgres_app.test_rate_limiting() +quart_postgres_app.test_blocking() diff --git a/sample-apps/quart-postgres-uvicorn/app.py b/sample-apps/quart-postgres-uvicorn/app.py index 65b59725..ec7bab2d 100644 --- a/sample-apps/quart-postgres-uvicorn/app.py +++ b/sample-apps/quart-postgres-uvicorn/app.py @@ -11,7 +11,9 @@ def __init__(self, app): self.app = app async def __call__(self, scope, receive, send): - aikido_zen.set_user({"id": "user123", "name": "John Doe"}) + for header, value in scope['headers']: + if header == 'User': + aikido_zen.set_user({"id": value, "name": "John Doe"}) return await self.app(scope, receive, send) app.asgi_app = AikidoQuartMiddleware(app.asgi_app) @@ -64,6 +66,10 @@ async def create_dog(): return jsonify({"message": f'Dog {dog_name} created successfully'}), 201 +@app.route("/test_ratelimiting_1", methods=['GET']) +async def test_ratelimiting_1(): + return jsonify({"message": "OK"}) + @app.route("/create_many", methods=['POST']) async def create_dog_many(): data = await request.form From 3d6f6a4afd2304a64b43ca646710f510696ff82c Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 10 Mar 2025 09:51:53 +0100 Subject: [PATCH 22/31] Starlette expand testing e2e --- end2end/starlette_postgres_uvicorn.py | 2 ++ sample-apps/starlette-postgres-uvicorn/app.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/end2end/starlette_postgres_uvicorn.py b/end2end/starlette_postgres_uvicorn.py index d0804de2..3420bd5c 100644 --- a/end2end/starlette_postgres_uvicorn.py +++ b/end2end/starlette_postgres_uvicorn.py @@ -10,3 +10,5 @@ ) starlette_postgres_app.test_all_payloads() +starlette_postgres_app.test_rate_limiting() +starlette_postgres_app.test_blocking() diff --git a/sample-apps/starlette-postgres-uvicorn/app.py b/sample-apps/starlette-postgres-uvicorn/app.py index d349154c..5f4cf24c 100644 --- a/sample-apps/starlette-postgres-uvicorn/app.py +++ b/sample-apps/starlette-postgres-uvicorn/app.py @@ -73,7 +73,9 @@ def __init__(self, app): self.app = app async def __call__(self, scope, receive, send): - aikido_zen.set_user({"id": "user123", "name": "John Doe"}) + for header, value in scope['headers']: + if header == 'User': + aikido_zen.set_user({"id": value, "name": "John Doe"}) return await self.app(scope, receive, send) middleware.append(Middleware(SetUserMiddleware)) middleware.append(Middleware(AikidoStarletteMiddleware)) @@ -87,6 +89,7 @@ async def __call__(self, scope, receive, send): Route("/create", create_dog, methods=["POST"]), Route("/sync_route", sync_route), Route("/just", just, methods=["GET"]), + Route("/test_ratelimiting_1", just, methods=["GET"]), Route("/delayed_route", delayed_route, methods=["GET"]) ] if len(middleware) != 0: From c6dc209f33ba2b3a8795da29c69059c3f8629d9e Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 10 Mar 2025 09:56:49 +0100 Subject: [PATCH 23/31] remove standard user present in quart attack --- end2end/attack_events.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/end2end/attack_events.json b/end2end/attack_events.json index bf0ad426..fc220469 100644 --- a/end2end/attack_events.json +++ b/end2end/attack_events.json @@ -41,8 +41,7 @@ "operation": "asyncpg.connection.Connection.execute", "pathToPayload": ".dog_name", "payload": "\"Dangerous Bobby', TRUE); -- \"", - "source": "body", - "user_id": "user123" + "source": "body" }, "flask_xml_attack": { "blocked": true, From b14a5c904f01f928033ebfb3a6422883858ca085 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 10 Mar 2025 09:57:17 +0100 Subject: [PATCH 24/31] end2end delay make it longer --- .github/workflows/end2end.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/end2end.yml b/.github/workflows/end2end.yml index b45f9fad..47792516 100644 --- a/.github/workflows/end2end.yml +++ b/.github/workflows/end2end.yml @@ -63,7 +63,7 @@ jobs: - name: Start application working-directory: ./sample-apps/${{ matrix.app.name }} run: | - nohup make run > output.log & tail -f output.log & sleep 20 + nohup make run > output.log & tail -f output.log & sleep 25 nohup make runZenDisabled & sleep 20 - name: Run end2end tests for application run: tail -f ./sample-apps/${{ matrix.app.name }}/output.log & python ./${{ matrix.app.testfile }} From 9992f817708e473d3081459a82a73e19499ddf28 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 10 Mar 2025 10:08:08 +0100 Subject: [PATCH 25/31] Fix setting of user --- sample-apps/quart-postgres-uvicorn/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sample-apps/quart-postgres-uvicorn/app.py b/sample-apps/quart-postgres-uvicorn/app.py index ec7bab2d..95526719 100644 --- a/sample-apps/quart-postgres-uvicorn/app.py +++ b/sample-apps/quart-postgres-uvicorn/app.py @@ -12,8 +12,8 @@ def __init__(self, app): async def __call__(self, scope, receive, send): for header, value in scope['headers']: - if header == 'User': - aikido_zen.set_user({"id": value, "name": "John Doe"}) + if header == b'User': + aikido_zen.set_user({"id": str(value), "name": "John Doe"}) return await self.app(scope, receive, send) app.asgi_app = AikidoQuartMiddleware(app.asgi_app) From 1baeccc3403c834d22e28f99a05a9c8cd42ec083 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 10 Mar 2025 10:15:43 +0100 Subject: [PATCH 26/31] Check headers for lowercase user header --- sample-apps/quart-postgres-uvicorn/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample-apps/quart-postgres-uvicorn/app.py b/sample-apps/quart-postgres-uvicorn/app.py index 95526719..28de7efb 100644 --- a/sample-apps/quart-postgres-uvicorn/app.py +++ b/sample-apps/quart-postgres-uvicorn/app.py @@ -12,7 +12,7 @@ def __init__(self, app): async def __call__(self, scope, receive, send): for header, value in scope['headers']: - if header == b'User': + if header == b'user': aikido_zen.set_user({"id": str(value), "name": "John Doe"}) return await self.app(scope, receive, send) From de7b3f43fd6241b7e1721e18c320f48ba89be53a Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 10 Mar 2025 10:17:12 +0100 Subject: [PATCH 27/31] sleep 30 seconds --- .github/workflows/end2end.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/end2end.yml b/.github/workflows/end2end.yml index 47792516..9604570a 100644 --- a/.github/workflows/end2end.yml +++ b/.github/workflows/end2end.yml @@ -63,7 +63,7 @@ jobs: - name: Start application working-directory: ./sample-apps/${{ matrix.app.name }} run: | - nohup make run > output.log & tail -f output.log & sleep 25 - nohup make runZenDisabled & sleep 20 + nohup make run > output.log & tail -f output.log & sleep 30 + nohup make runZenDisabled & sleep 30 - name: Run end2end tests for application run: tail -f ./sample-apps/${{ matrix.app.name }}/output.log & python ./${{ matrix.app.testfile }} From dc81d87eb8f1351be7f86a7c1787dbd9d854a7c5 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 10 Mar 2025 10:21:24 +0100 Subject: [PATCH 28/31] print the headers in quart app for SetUser --- sample-apps/quart-postgres-uvicorn/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sample-apps/quart-postgres-uvicorn/app.py b/sample-apps/quart-postgres-uvicorn/app.py index 28de7efb..28d3c852 100644 --- a/sample-apps/quart-postgres-uvicorn/app.py +++ b/sample-apps/quart-postgres-uvicorn/app.py @@ -11,8 +11,9 @@ def __init__(self, app): self.app = app async def __call__(self, scope, receive, send): + print(scope['headers']) for header, value in scope['headers']: - if header == b'user': + if header == b'USER': aikido_zen.set_user({"id": str(value), "name": "John Doe"}) return await self.app(scope, receive, send) From d5a71d49ebf36ed0eda289738577c7b83f402d1a Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 10 Mar 2025 10:26:12 +0100 Subject: [PATCH 29/31] set users dynamically in both quart and starlette apps --- sample-apps/quart-postgres-uvicorn/app.py | 5 ++--- sample-apps/starlette-postgres-uvicorn/app.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/sample-apps/quart-postgres-uvicorn/app.py b/sample-apps/quart-postgres-uvicorn/app.py index 28d3c852..a1385a52 100644 --- a/sample-apps/quart-postgres-uvicorn/app.py +++ b/sample-apps/quart-postgres-uvicorn/app.py @@ -11,10 +11,9 @@ def __init__(self, app): self.app = app async def __call__(self, scope, receive, send): - print(scope['headers']) for header, value in scope['headers']: - if header == b'USER': - aikido_zen.set_user({"id": str(value), "name": "John Doe"}) + if header.decode("utf-8").upper() == 'USER': + aikido_zen.set_user({"id": value.decode("utf-8"), "name": "John Doe"}) return await self.app(scope, receive, send) app.asgi_app = AikidoQuartMiddleware(app.asgi_app) diff --git a/sample-apps/starlette-postgres-uvicorn/app.py b/sample-apps/starlette-postgres-uvicorn/app.py index 5f4cf24c..104cab15 100644 --- a/sample-apps/starlette-postgres-uvicorn/app.py +++ b/sample-apps/starlette-postgres-uvicorn/app.py @@ -74,8 +74,8 @@ def __init__(self, app): async def __call__(self, scope, receive, send): for header, value in scope['headers']: - if header == 'User': - aikido_zen.set_user({"id": value, "name": "John Doe"}) + if header.decode("utf-8").upper() == 'USER': + aikido_zen.set_user({"id": value.decode("utf-8"), "name": "John Doe"}) return await self.app(scope, receive, send) middleware.append(Middleware(SetUserMiddleware)) middleware.append(Middleware(AikidoStarletteMiddleware)) From 380b826d8791beccacc2d8b65d89910c7f5df6ac Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 10 Mar 2025 10:52:50 +0100 Subject: [PATCH 30/31] remove test_blocking from flask mysql --- end2end/flask_mysql.py | 1 - 1 file changed, 1 deletion(-) diff --git a/end2end/flask_mysql.py b/end2end/flask_mysql.py index f6f78f40..5e9a2b9f 100644 --- a/end2end/flask_mysql.py +++ b/end2end/flask_mysql.py @@ -16,4 +16,3 @@ flask_mysql_app.test_all_payloads() flask_mysql_app.test_rate_limiting() -flask_mysql_app.test_blocking() From 21c6aee09139334e966bbb8cc32ff4818d7a50a4 Mon Sep 17 00:00:00 2001 From: Wout Feys Date: Mon, 10 Mar 2025 10:53:37 +0100 Subject: [PATCH 31/31] Remove test_blocking from quart and starlette apps as well --- end2end/quart_postgres_uvicorn.py | 1 - end2end/starlette_postgres_uvicorn.py | 1 - 2 files changed, 2 deletions(-) diff --git a/end2end/quart_postgres_uvicorn.py b/end2end/quart_postgres_uvicorn.py index a099f5d9..6143d047 100644 --- a/end2end/quart_postgres_uvicorn.py +++ b/end2end/quart_postgres_uvicorn.py @@ -11,4 +11,3 @@ quart_postgres_app.test_all_payloads() quart_postgres_app.test_rate_limiting() -quart_postgres_app.test_blocking() diff --git a/end2end/starlette_postgres_uvicorn.py b/end2end/starlette_postgres_uvicorn.py index 3420bd5c..f64d72e4 100644 --- a/end2end/starlette_postgres_uvicorn.py +++ b/end2end/starlette_postgres_uvicorn.py @@ -11,4 +11,3 @@ starlette_postgres_app.test_all_payloads() starlette_postgres_app.test_rate_limiting() -starlette_postgres_app.test_blocking()