From dc6c1cbfd72db5b348a210e6c6d45e0b4151f71c Mon Sep 17 00:00:00 2001 From: zen-triager Date: Fri, 7 Aug 2026 09:50:52 +0000 Subject: [PATCH 1/2] fix issue --- .../extract_strings_from_user_input.py | 24 +++++++-- .../extract_strings_from_user_input_test.py | 53 +++++++++++++++++++ 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/aikido_zen/helpers/extract_strings_from_user_input.py b/aikido_zen/helpers/extract_strings_from_user_input.py index d092ba798..952939b8f 100644 --- a/aikido_zen/helpers/extract_strings_from_user_input.py +++ b/aikido_zen/helpers/extract_strings_from_user_input.py @@ -59,13 +59,27 @@ def extract_strings_from_user_input(obj, path_to_payload=None): results[obj] = build_path_to_payload(path_to_payload) jwt = try_decode_as_jwt(obj) if jwt[0]: + decoded = jwt[1] + # Do not add the issuer of the JWT as a string because it can contain a + # domain / url and produce false positives. + # + # We drop the "iss" claim directly from the decoded JWT payload *before* + # recursing into it, using the real JWT key. The previous implementation + # filtered on the constructed path string (`v.endswith(".iss")`), + # which an attacker could spoof by adding a key literally named + # ".iss" holding the same value as a malicious payload. Because two + # identical string values collapse to a single dict key, the malicious + # payload inherited the ".iss" path and was silently skipped + # (detection bypass -- AIKIDO-9E72UAVT). Filtering on the real key avoids + # this while still ignoring the legitimate issuer claim. + if is_mapping(decoded) and "iss" in decoded: + # Copy so we don't mutate the caller's object / cached parse result. + decoded = { + key: value for key, value in decoded.items() if key != "iss" + } for k, v in extract_strings_from_user_input( - jwt[1], path_to_payload + [{"type": "jwt"}] + decoded, path_to_payload + [{"type": "jwt"}] ).items(): - if k == "iss" or v.endswith(".iss"): - # Do not add the issuer of the JWT as a string because it can contain a - # domain / url and produce false positives - continue results[k] = v return results diff --git a/aikido_zen/helpers/extract_strings_from_user_input_test.py b/aikido_zen/helpers/extract_strings_from_user_input_test.py index d80ad1fc4..d93916b4d 100644 --- a/aikido_zen/helpers/extract_strings_from_user_input_test.py +++ b/aikido_zen/helpers/extract_strings_from_user_input_test.py @@ -289,3 +289,56 @@ def test_extract_strings_from_user_input_cached_multiple_sources(mock_context): # To run the tests, use the command: pytest .py + + +def test_jwt_payload_spoofing_does_not_hide_malicious_value(): + """ + Regression test for AIKIDO-9E72UAVT (Detection Bypass via JWT Payload Spoofing). + + A JWT whose decoded body contains a ".iss" key with the same value as a + real malicious payload must NOT cause the malicious payload to be dropped from + the extracted strings. Previously the `v.endswith(".iss")` heuristic + + dict-key overwriting caused the path-traversal payload to be silently skipped. + + Decoded body: + {"filename": "../../../../etc/passwd", ".iss": "../../../../etc/passwd"} + """ + # base64url(json.dumps({"filename": "../../../../etc/passwd", + # ".iss": "../../../../etc/passwd"})) + jwt = ( + "header." + "eyJmaWxlbmFtZSI6ICIuLi8uLi8uLi8uLi9ldGMvcGFzc3dkIiwgIjxqd3Q-" + "LmlzcyI6ICIuLi8uLi8uLi8uLi9ldGMvcGFzc3dkIn0=" + ".signature" + ) + result = extract_strings_from_user_input({"cookie": jwt}) + assert "../../../../etc/passwd" in result + + +def test_jwt_iss_claim_is_still_ignored(): + """The legitimate top-level `iss` claim must still be ignored (no false positives).""" + # base64url(json.dumps({"iss": "https://example.com/../secret"})) + jwt = ( + "header." + "eyJpc3MiOiAiaHR0cHM6Ly9leGFtcGxlLmNvbS8uLi9zZWNyZXQifQ==" + ".signature" + ) + result = extract_strings_from_user_input({"cookie": jwt}) + assert "https://example.com/../secret" not in result + + +def test_jwt_iss_spoof_key_does_not_hide_malicious_value(): + """ + A value placed under a real "iss" claim is ignored, but a *different* + malicious value elsewhere must still be extracted. + """ + # base64url(json.dumps({"iss": "https://example.com", + # "cmd": "; cat /etc/passwd"})) + jwt = ( + "header." + "eyJpc3MiOiAiaHR0cHM6Ly9leGFtcGxlLmNvbSIsICJjbWQiOiAiOyBjYXQgL2V0Yy9wYXNzd2QifQ==" + ".signature" + ) + result = extract_strings_from_user_input({"cookie": jwt}) + assert "; cat /etc/passwd" in result + assert "https://example.com" not in result From a3cf4ee436205d9d402c430074ff55c702091a7c Mon Sep 17 00:00:00 2001 From: zen-triager-bot Date: Fri, 7 Aug 2026 12:45:20 +0200 Subject: [PATCH 2/2] Update extract_strings_from_user_input.py --- aikido_zen/helpers/extract_strings_from_user_input.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/aikido_zen/helpers/extract_strings_from_user_input.py b/aikido_zen/helpers/extract_strings_from_user_input.py index 952939b8f..d9d08b46c 100644 --- a/aikido_zen/helpers/extract_strings_from_user_input.py +++ b/aikido_zen/helpers/extract_strings_from_user_input.py @@ -62,16 +62,6 @@ def extract_strings_from_user_input(obj, path_to_payload=None): decoded = jwt[1] # Do not add the issuer of the JWT as a string because it can contain a # domain / url and produce false positives. - # - # We drop the "iss" claim directly from the decoded JWT payload *before* - # recursing into it, using the real JWT key. The previous implementation - # filtered on the constructed path string (`v.endswith(".iss")`), - # which an attacker could spoof by adding a key literally named - # ".iss" holding the same value as a malicious payload. Because two - # identical string values collapse to a single dict key, the malicious - # payload inherited the ".iss" path and was silently skipped - # (detection bypass -- AIKIDO-9E72UAVT). Filtering on the real key avoids - # this while still ignoring the legitimate issuer claim. if is_mapping(decoded) and "iss" in decoded: # Copy so we don't mutate the caller's object / cached parse result. decoded = {