From b0e2548389e4088d6446232664522423a2b0bb96 Mon Sep 17 00:00:00 2001 From: Christophe Papazian Date: Fri, 5 Dec 2025 14:38:25 +0100 Subject: [PATCH 1/7] chore(aap): improve api10 redirection analysis --- ddtrace/appsec/_common_module_patches.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/ddtrace/appsec/_common_module_patches.py b/ddtrace/appsec/_common_module_patches.py index 50253370207..08794f7b9b4 100644 --- a/ddtrace/appsec/_common_module_patches.py +++ b/ddtrace/appsec/_common_module_patches.py @@ -59,6 +59,7 @@ def _(module): try_wrap_function_wrapper("builtins", "open", wrapped_open_CFDDB7ABBA9081B6) try_wrap_function_wrapper("urllib.request", "OpenerDirector.open", wrapped_open_ED4CF71136E15EBF) try_wrap_function_wrapper("http.client", "HTTPConnection.request", wrapped_request) + try_wrap_function_wrapper("http.client", "HTTPConnection.getresponse", wrapped_response) core.on("asm.block.dbapi.execute", execute_4C9BAC8E228EB347) log.debug("Patching common modules: builtins and urllib.request") _is_patched = True @@ -74,6 +75,8 @@ def unpatch_common_modules(): try_unwrap("urllib3.request", "RequestMethods.request") try_unwrap("builtins", "open") try_unwrap("urllib.request", "OpenerDirector.open") + try_unwrap("http.client", "HTTPConnection.request") + try_unwrap("http.client", "HTTPConnection.getresponse") try_unwrap("_io", "BytesIO.read") try_unwrap("_io", "StringIO.read") subprocess_patch.unpatch() @@ -190,6 +193,26 @@ def wrapped_request(original_request_callable, instance, args, kwargs): return original_request_callable(*args, **kwargs) +def wrapped_response(original_response_callable, instance, args, kwargs): + from ddtrace.appsec._asm_request_context import call_waf_callback + + response = original_response_callable(*args, *kwargs) + env = _get_asm_context() + try: + if _get_rasp_capability("ssrf") and response.__class__.__name__ == "HTTPResponse" and env is not None: + status = response.getcode() + if 300 <= status < 400: + # api10 for redirected response status and headers in urllib + addresses = { + "DOWN_RES_STATUS": str(status), + "DOWN_RES_HEADERS": _build_headers(response.getheaders()), + } + call_waf_callback(addresses, rule_type=EXPLOIT_PREVENTION.TYPE.SSRF_RES) + except Exception: + pass + return response + + def _parse_http_response_body(response): try: if response.length and response.headers.get("content-type", None) == "application/json": From 84bd658de1d6d5be78e454826732563d164c1c73 Mon Sep 17 00:00:00 2001 From: Christophe Papazian Date: Fri, 5 Dec 2025 15:26:42 +0100 Subject: [PATCH 2/7] ensure redirect are not analyzed twice --- ddtrace/appsec/_common_module_patches.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddtrace/appsec/_common_module_patches.py b/ddtrace/appsec/_common_module_patches.py index 08794f7b9b4..d0b9f3b44f4 100644 --- a/ddtrace/appsec/_common_module_patches.py +++ b/ddtrace/appsec/_common_module_patches.py @@ -251,7 +251,7 @@ def wrapped_open_ED4CF71136E15EBF(original_open_callable, instance, args, kwargs try: response = original_open_callable(*args, **kwargs) # api10 response handler for regular responses - if response.__class__.__name__ == "HTTPResponse": + if response.__class__.__name__ == "HTTPResponse" and not 300<=response.status<400: addresses = { "DOWN_RES_STATUS": str(response.status), "DOWN_RES_HEADERS": _build_headers(response.getheaders()), From 22f12d179f0ed685095e4750a064e6c132fecd3a Mon Sep 17 00:00:00 2001 From: Christophe Papazian Date: Fri, 5 Dec 2025 16:53:38 +0100 Subject: [PATCH 3/7] adding redirect response analysis to urllib3 --- ddtrace/appsec/_common_module_patches.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/ddtrace/appsec/_common_module_patches.py b/ddtrace/appsec/_common_module_patches.py index d0b9f3b44f4..0c449d5275a 100644 --- a/ddtrace/appsec/_common_module_patches.py +++ b/ddtrace/appsec/_common_module_patches.py @@ -251,7 +251,8 @@ def wrapped_open_ED4CF71136E15EBF(original_open_callable, instance, args, kwargs try: response = original_open_callable(*args, **kwargs) # api10 response handler for regular responses - if response.__class__.__name__ == "HTTPResponse" and not 300<=response.status<400: + if response.__class__.__name__ == "HTTPResponse" and not (300<=response.status<400) + : addresses = { "DOWN_RES_STATUS": str(response.status), "DOWN_RES_HEADERS": _build_headers(response.getheaders()), @@ -294,7 +295,8 @@ def wrapped_urllib3_make_request(original_request_callable, instance, args, kwar full_url = core.get_item("full_url") env = _get_asm_context() - if _get_rasp_capability("ssrf") and full_url is not None and env is not None: + do_rasp = _get_rasp_capability("ssrf") and full_url is not None and env is not None + if do_rasp: use_body = core.get_item("use_body", False) method = args[1] if len(args) > 1 else kwargs.get("method", None) body = args[3] if len(args) > 3 else kwargs.get("body", None) @@ -315,7 +317,17 @@ def wrapped_urllib3_make_request(original_request_callable, instance, args, kwar core.discard_item("full_url") if res and _must_block(res.actions): raise BlockingException(get_blocked(), EXPLOIT_PREVENTION.BLOCKING, EXPLOIT_PREVENTION.TYPE.SSRF, full_url) - return original_request_callable(*args, **kwargs) + response = original_request_callable(*args, **kwargs) + try: + if do_rasp and response.__class__.__name__ == "BaseHTTPResponse" and 300<=response.status<400: + # api10 for redirected response status and headers in urllib3 + addresses = { + "DOWN_RES_STATUS": str(response.status), + "DOWN_RES_HEADERS": response.headers, + } + call_waf_callback(addresses, rule_type=EXPLOIT_PREVENTION.TYPE.SSRF_RES) + finally: + return response def wrapped_urllib3_urlopen(original_open_callable, instance, args, kwargs): @@ -352,7 +364,7 @@ def wrapped_request_D8CB81E472AF98A2(original_request_callable, instance, args, # API10, doing all request calls in HTTPConnection.request try: response = original_request_callable(*args, **kwargs) - if response.__class__.__name__ == "Response": + if response.__class__.__name__ == "Response" and not (300<=response.status_code<400): addresses = { "DOWN_RES_STATUS": str(response.status_code), "DOWN_RES_HEADERS": dict(response.headers), From b0d48e56c3a6ed4c597c6942f29537ff455cd75f Mon Sep 17 00:00:00 2001 From: Christophe Papazian Date: Fri, 5 Dec 2025 16:58:13 +0100 Subject: [PATCH 4/7] lint --- ddtrace/appsec/_common_module_patches.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/ddtrace/appsec/_common_module_patches.py b/ddtrace/appsec/_common_module_patches.py index 0c449d5275a..7c93a1e4b1b 100644 --- a/ddtrace/appsec/_common_module_patches.py +++ b/ddtrace/appsec/_common_module_patches.py @@ -251,8 +251,7 @@ def wrapped_open_ED4CF71136E15EBF(original_open_callable, instance, args, kwargs try: response = original_open_callable(*args, **kwargs) # api10 response handler for regular responses - if response.__class__.__name__ == "HTTPResponse" and not (300<=response.status<400) - : + if response.__class__.__name__ == "HTTPResponse" and not (300 <= response.status < 400): addresses = { "DOWN_RES_STATUS": str(response.status), "DOWN_RES_HEADERS": _build_headers(response.getheaders()), @@ -319,12 +318,12 @@ def wrapped_urllib3_make_request(original_request_callable, instance, args, kwar raise BlockingException(get_blocked(), EXPLOIT_PREVENTION.BLOCKING, EXPLOIT_PREVENTION.TYPE.SSRF, full_url) response = original_request_callable(*args, **kwargs) try: - if do_rasp and response.__class__.__name__ == "BaseHTTPResponse" and 300<=response.status<400: + if do_rasp and response.__class__.__name__ == "BaseHTTPResponse" and 300 <= response.status < 400: # api10 for redirected response status and headers in urllib3 addresses = { - "DOWN_RES_STATUS": str(response.status), - "DOWN_RES_HEADERS": response.headers, - } + "DOWN_RES_STATUS": str(response.status), + "DOWN_RES_HEADERS": response.headers, + } call_waf_callback(addresses, rule_type=EXPLOIT_PREVENTION.TYPE.SSRF_RES) finally: return response @@ -364,7 +363,7 @@ def wrapped_request_D8CB81E472AF98A2(original_request_callable, instance, args, # API10, doing all request calls in HTTPConnection.request try: response = original_request_callable(*args, **kwargs) - if response.__class__.__name__ == "Response" and not (300<=response.status_code<400): + if response.__class__.__name__ == "Response" and not (300 <= response.status_code < 400): addresses = { "DOWN_RES_STATUS": str(response.status_code), "DOWN_RES_HEADERS": dict(response.headers), From a8df38df0afb8f56d52d2417528a746f3046dfd5 Mon Sep 17 00:00:00 2001 From: Christophe Papazian Date: Fri, 5 Dec 2025 17:05:10 +0100 Subject: [PATCH 5/7] no sec --- ddtrace/appsec/_common_module_patches.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddtrace/appsec/_common_module_patches.py b/ddtrace/appsec/_common_module_patches.py index 7c93a1e4b1b..5a71d53c2bf 100644 --- a/ddtrace/appsec/_common_module_patches.py +++ b/ddtrace/appsec/_common_module_patches.py @@ -209,7 +209,7 @@ def wrapped_response(original_response_callable, instance, args, kwargs): } call_waf_callback(addresses, rule_type=EXPLOIT_PREVENTION.TYPE.SSRF_RES) except Exception: - pass + pass # nosec return response From 151b22b1fa21f2f356acf945f7406f941e99eb76 Mon Sep 17 00:00:00 2001 From: Christophe Papazian Date: Fri, 5 Dec 2025 17:08:28 +0100 Subject: [PATCH 6/7] lint --- ddtrace/appsec/_common_module_patches.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddtrace/appsec/_common_module_patches.py b/ddtrace/appsec/_common_module_patches.py index 5a71d53c2bf..aa254086edb 100644 --- a/ddtrace/appsec/_common_module_patches.py +++ b/ddtrace/appsec/_common_module_patches.py @@ -209,7 +209,7 @@ def wrapped_response(original_response_callable, instance, args, kwargs): } call_waf_callback(addresses, rule_type=EXPLOIT_PREVENTION.TYPE.SSRF_RES) except Exception: - pass # nosec + pass # nosec return response From 374bdb19ba59af534be2cad75b33bdaab85d5417 Mon Sep 17 00:00:00 2001 From: Christophe Papazian Date: Fri, 5 Dec 2025 17:11:59 +0100 Subject: [PATCH 7/7] fix ast --- ddtrace/appsec/_common_module_patches.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ddtrace/appsec/_common_module_patches.py b/ddtrace/appsec/_common_module_patches.py index aa254086edb..4a863070fbc 100644 --- a/ddtrace/appsec/_common_module_patches.py +++ b/ddtrace/appsec/_common_module_patches.py @@ -325,8 +325,9 @@ def wrapped_urllib3_make_request(original_request_callable, instance, args, kwar "DOWN_RES_HEADERS": response.headers, } call_waf_callback(addresses, rule_type=EXPLOIT_PREVENTION.TYPE.SSRF_RES) - finally: - return response + except Exception: + pass # nosec + return response def wrapped_urllib3_urlopen(original_open_callable, instance, args, kwargs):