Skip to content

Commit b93a092

Browse files
chore(aap): improve api10 redirection analysis (#15529)
## Description Add redirection response waf analysis both to urllib and urllib3 ## Testing This will be tested by DataDog/system-tests#5831 APPSEC-60065
1 parent fdd8fed commit b93a092

File tree

1 file changed

+39
-4
lines changed

1 file changed

+39
-4
lines changed

ddtrace/appsec/_common_module_patches.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def _(module):
5959
try_wrap_function_wrapper("builtins", "open", wrapped_open_CFDDB7ABBA9081B6)
6060
try_wrap_function_wrapper("urllib.request", "OpenerDirector.open", wrapped_open_ED4CF71136E15EBF)
6161
try_wrap_function_wrapper("http.client", "HTTPConnection.request", wrapped_request)
62+
try_wrap_function_wrapper("http.client", "HTTPConnection.getresponse", wrapped_response)
6263
core.on("asm.block.dbapi.execute", execute_4C9BAC8E228EB347)
6364
log.debug("Patching common modules: builtins and urllib.request")
6465
_is_patched = True
@@ -74,6 +75,8 @@ def unpatch_common_modules():
7475
try_unwrap("urllib3.request", "RequestMethods.request")
7576
try_unwrap("builtins", "open")
7677
try_unwrap("urllib.request", "OpenerDirector.open")
78+
try_unwrap("http.client", "HTTPConnection.request")
79+
try_unwrap("http.client", "HTTPConnection.getresponse")
7780
try_unwrap("_io", "BytesIO.read")
7881
try_unwrap("_io", "StringIO.read")
7982
subprocess_patch.unpatch()
@@ -190,6 +193,26 @@ def wrapped_request(original_request_callable, instance, args, kwargs):
190193
return original_request_callable(*args, **kwargs)
191194

192195

196+
def wrapped_response(original_response_callable, instance, args, kwargs):
197+
from ddtrace.appsec._asm_request_context import call_waf_callback
198+
199+
response = original_response_callable(*args, *kwargs)
200+
env = _get_asm_context()
201+
try:
202+
if _get_rasp_capability("ssrf") and response.__class__.__name__ == "HTTPResponse" and env is not None:
203+
status = response.getcode()
204+
if 300 <= status < 400:
205+
# api10 for redirected response status and headers in urllib
206+
addresses = {
207+
"DOWN_RES_STATUS": str(status),
208+
"DOWN_RES_HEADERS": _build_headers(response.getheaders()),
209+
}
210+
call_waf_callback(addresses, rule_type=EXPLOIT_PREVENTION.TYPE.SSRF_RES)
211+
except Exception:
212+
pass # nosec
213+
return response
214+
215+
193216
def _parse_http_response_body(response):
194217
try:
195218
if response.length and response.headers.get("content-type", None) == "application/json":
@@ -228,7 +251,7 @@ def wrapped_open_ED4CF71136E15EBF(original_open_callable, instance, args, kwargs
228251
try:
229252
response = original_open_callable(*args, **kwargs)
230253
# api10 response handler for regular responses
231-
if response.__class__.__name__ == "HTTPResponse":
254+
if response.__class__.__name__ == "HTTPResponse" and not (300 <= response.status < 400):
232255
addresses = {
233256
"DOWN_RES_STATUS": str(response.status),
234257
"DOWN_RES_HEADERS": _build_headers(response.getheaders()),
@@ -271,7 +294,8 @@ def wrapped_urllib3_make_request(original_request_callable, instance, args, kwar
271294

272295
full_url = core.get_item("full_url")
273296
env = _get_asm_context()
274-
if _get_rasp_capability("ssrf") and full_url is not None and env is not None:
297+
do_rasp = _get_rasp_capability("ssrf") and full_url is not None and env is not None
298+
if do_rasp:
275299
use_body = core.get_item("use_body", False)
276300
method = args[1] if len(args) > 1 else kwargs.get("method", None)
277301
body = args[3] if len(args) > 3 else kwargs.get("body", None)
@@ -292,7 +316,18 @@ def wrapped_urllib3_make_request(original_request_callable, instance, args, kwar
292316
core.discard_item("full_url")
293317
if res and _must_block(res.actions):
294318
raise BlockingException(get_blocked(), EXPLOIT_PREVENTION.BLOCKING, EXPLOIT_PREVENTION.TYPE.SSRF, full_url)
295-
return original_request_callable(*args, **kwargs)
319+
response = original_request_callable(*args, **kwargs)
320+
try:
321+
if do_rasp and response.__class__.__name__ == "BaseHTTPResponse" and 300 <= response.status < 400:
322+
# api10 for redirected response status and headers in urllib3
323+
addresses = {
324+
"DOWN_RES_STATUS": str(response.status),
325+
"DOWN_RES_HEADERS": response.headers,
326+
}
327+
call_waf_callback(addresses, rule_type=EXPLOIT_PREVENTION.TYPE.SSRF_RES)
328+
except Exception:
329+
pass # nosec
330+
return response
296331

297332

298333
def wrapped_urllib3_urlopen(original_open_callable, instance, args, kwargs):
@@ -329,7 +364,7 @@ def wrapped_request_D8CB81E472AF98A2(original_request_callable, instance, args,
329364
# API10, doing all request calls in HTTPConnection.request
330365
try:
331366
response = original_request_callable(*args, **kwargs)
332-
if response.__class__.__name__ == "Response":
367+
if response.__class__.__name__ == "Response" and not (300 <= response.status_code < 400):
333368
addresses = {
334369
"DOWN_RES_STATUS": str(response.status_code),
335370
"DOWN_RES_HEADERS": dict(response.headers),

0 commit comments

Comments
 (0)