Skip to content

Commit 30bc365

Browse files
committed
Include query string in URL for ASGI/WSGI requests
The request URL captured for attack events was missing the query string, causing path-traversal events (and other query-driven attacks) to lose the malicious payload from their reported URL. Re-enables test_path_traversal in the QA suite.
1 parent c77efdf commit 30bc365

8 files changed

Lines changed: 65 additions & 10 deletions

File tree

.github/workflows/qa-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,4 @@ jobs:
5252
app_port: 8080
5353
sleep_before_test: 30
5454
config_update_delay: 100
55-
skip_tests: test_bypassed_ip_for_geo_blocking,test_demo_apps_generic_tests,test_path_traversal,test_outbound_domain_blocking,test_bypassed_ip,test_wave_attack,test_block_traffic_by_countries,test_user_rate_limiting_1_minute
55+
skip_tests: test_bypassed_ip_for_geo_blocking,test_demo_apps_generic_tests,test_outbound_domain_blocking,test_bypassed_ip,test_wave_attack,test_block_traffic_by_countries,test_user_rate_limiting_1_minute

aikido_zen/context/asgi/build_url_from_asgi.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,9 @@ def build_url_from_asgi(scope):
1414
root_path = scope.get("root_path", "")
1515
path = scope.get("path", "")
1616
uri = path.replace(root_path, "", 1)
17+
18+
query_string = scope.get("query_string") or b""
19+
if query_string:
20+
uri = f"{uri}?{query_string.decode('utf-8', errors='replace')}"
21+
1722
return f"{scheme}://{host}{uri}"

aikido_zen/context/asgi/build_url_from_asgi_test.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,32 @@ def test_build_url_from_asgi_with_query_string():
117117
"scheme": "http",
118118
"server": ("localhost", 8000),
119119
"root_path": "",
120-
"path": "/api/v1/resource?query=1",
120+
"path": "/api/v1/resource",
121+
"query_string": b"query=1",
121122
}
122123
expected = "http://localhost:8000/api/v1/resource?query=1"
123124
assert build_url_from_asgi(scope) == expected
125+
126+
127+
def test_build_url_from_asgi_with_empty_query_string():
128+
scope = {
129+
"scheme": "http",
130+
"server": ("localhost", 8000),
131+
"root_path": "",
132+
"path": "/api/v1/resource",
133+
"query_string": b"",
134+
}
135+
expected = "http://localhost:8000/api/v1/resource"
136+
assert build_url_from_asgi(scope) == expected
137+
138+
139+
def test_build_url_from_asgi_path_traversal_query():
140+
scope = {
141+
"scheme": "http",
142+
"server": ("localhost", 3018),
143+
"root_path": "",
144+
"path": "/api/read",
145+
"query_string": b"path=../secrets/key.txt",
146+
}
147+
expected = "http://localhost:3018/api/read?path=../secrets/key.txt"
148+
assert build_url_from_asgi(scope) == expected

aikido_zen/context/asgi/init_test.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def test_asgi_scope_1():
3636
"HEADER1_TEST_2": ["testValue2198&"],
3737
}
3838
assert context1.cookies == {"a": "b", "c": "d"}
39-
assert context1.url == "https://192.168.0.1:443/a/b/c/d"
39+
assert context1.url == "https://192.168.0.1:443/a/b/c/d?a=b&b=d"
4040

4141

4242
# Scope 2 :
@@ -63,7 +63,7 @@ def test_asgi_scope_2():
6363
"HEADER2_TEST_1": ["anotherValue"],
6464
}
6565
assert context2.cookies == {"x": "y", "z": "w"}
66-
assert context2.url == "http://192.168.0.2:80/path/to/resource"
66+
assert context2.url == "http://192.168.0.2:80/path/to/resource?x=y&z=w"
6767

6868

6969
# Scope 3 :
@@ -90,7 +90,7 @@ def test_asgi_scope_3():
9090
"HEADER3_TEST_3": ["postValue"],
9191
}
9292
assert context3.cookies == {"session": "abc123"}
93-
assert context3.url == "http://192.168.0.3:8080/v1/resource"
93+
assert context3.url == "http://192.168.0.3:8080/v1/resource?key1=value1&key2=value2"
9494

9595

9696
# Scope 4 :

aikido_zen/context/init_test.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def test_wsgi_context_1():
5959
"CONTENT_TYPE": ["application/x-www-form-urlencoded"],
6060
},
6161
"cookies": {"sessionId": "abc123xyz456"},
62-
"url": "https://example.com/hello",
62+
"url": "https://example.com/hello?user=JohnDoe&age=30&age=35",
6363
"query": {"user": ["JohnDoe"], "age": ["30", "35"]},
6464
"body": 123,
6565
"route": "/hello",
@@ -91,7 +91,7 @@ def test_wsgi_context_2():
9191
"USER_AGENT": ["Mozilla/5.0"],
9292
},
9393
"cookies": {"sessionId": "abc123xyz456"},
94-
"url": "http://localhost:8080/hello",
94+
"url": "http://localhost:8080/hello?user=JohnDoe&age=30&age=35",
9595
"query": {"user": ["JohnDoe"], "age": ["30", "35"]},
9696
"body": {"test": True},
9797
"route": "/hello",
@@ -130,7 +130,7 @@ def test_context_is_picklable(mocker):
130130
assert unpickled_obj.source == "flask"
131131
assert unpickled_obj.method == "GET"
132132
assert unpickled_obj.remote_address == "198.51.100.23"
133-
assert unpickled_obj.url == "http://localhost:8080/hello"
133+
assert unpickled_obj.url == "http://localhost:8080/hello?user=JohnDoe&age=30&age=35"
134134
assert unpickled_obj.body == 123
135135
assert unpickled_obj.headers == {
136136
"HEADER_1": ["header 1 value"],

aikido_zen/context/wsgi/build_url_from_wsgi.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,8 @@ def build_url_from_wsgi(request):
1515
else:
1616
host = request["HTTP_HOST"]
1717

18+
query_string = request.get("QUERY_STRING", "")
19+
if query_string:
20+
uri = f"{uri}?{query_string}"
21+
1822
return f"{scheme}://{host}{uri}"

aikido_zen/context/wsgi/build_url_from_wsgi_test.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,32 @@ def test_build_url_from_wsgi_with_query_string():
5252
"PATH_INFO": "/search",
5353
"QUERY_STRING": "q=test",
5454
}
55-
# Note: The function does not currently handle query strings, so we won't include it in the expected output
55+
expected = "http://example.com/search?q=test"
56+
assert build_url_from_wsgi(request) == expected
57+
58+
59+
def test_build_url_from_wsgi_with_empty_query_string():
60+
request = {
61+
"wsgi.url_scheme": "http",
62+
"HTTP_HOST": "example.com",
63+
"PATH_INFO": "/search",
64+
"QUERY_STRING": "",
65+
}
5666
expected = "http://example.com/search"
5767
assert build_url_from_wsgi(request) == expected
5868

5969

70+
def test_build_url_from_wsgi_path_traversal_query():
71+
request = {
72+
"wsgi.url_scheme": "http",
73+
"HTTP_HOST": "localhost:3018",
74+
"PATH_INFO": "/api/read",
75+
"QUERY_STRING": "path=../secrets/key.txt",
76+
}
77+
expected = "http://localhost:3018/api/read?path=../secrets/key.txt"
78+
assert build_url_from_wsgi(request) == expected
79+
80+
6081
def test_build_url_from_wsgi_root_path():
6182
request = {"wsgi.url_scheme": "http", "HTTP_HOST": "example.com", "PATH_INFO": "/"}
6283
expected = "http://example.com/"

aikido_zen/vulnerabilities/init_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def test_sql_injection_with_comms(caplog, get_context, monkeypatch):
150150
"method": "GET",
151151
"route": "/hello",
152152
"source": "flask",
153-
"url": "http://localhost:8080/hello",
153+
"url": "http://localhost:8080/hello?user=JohnDoe&age=30&age=35",
154154
"userAgent": None,
155155
}
156156
del call_args[1].event["attack"]["stack"] # Hard to test

0 commit comments

Comments
 (0)