Skip to content

Commit 6d3183d

Browse files
fix(asm): capture XML parsing errors on body parse (#4559) (#4574)
* fix(asm): capture XML parsing errors on body parse Signed-off-by: Juanjo Alvarez <[email protected]> * chore(asm): add some tests for bad XML not raising an exception but logging a warning Signed-off-by: Juanjo Alvarez <[email protected]> * Update ddtrace/contrib/django/utils.py Co-authored-by: Brett Langdon <[email protected]> * Update releasenotes/notes/capture-xml-parsing-errors-e6c8c761ed026ce3.yaml Co-authored-by: Brett Langdon <[email protected]> * chore(asm): fix empty xml body flask test Signed-off-by: Juanjo Alvarez <[email protected]> * chore(asm): workaround some odd interaction between caplog and hypothesis Signed-off-by: Juanjo Alvarez <[email protected]> Signed-off-by: Juanjo Alvarez <[email protected]> Co-authored-by: Brett Langdon <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> (cherry picked from commit 91e5dfd) Co-authored-by: Juanjo Alvarez Martinez <[email protected]>
1 parent 56b6086 commit 6d3183d

File tree

6 files changed

+65
-5
lines changed

6 files changed

+65
-5
lines changed

ddtrace/contrib/django/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,8 @@ def _extract_body(request):
274274
OSError,
275275
ValueError,
276276
JSONDecodeError,
277+
xmltodict.expat.ExpatError,
278+
xmltodict.ParsingInterrupted,
277279
):
278280
log.warning("Failed to parse request body")
279281
# req_body is None

ddtrace/contrib/flask/patch.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,16 @@ def traced_wsgi_app(pin, wrapped, instance, args, kwargs):
388388
req_body = request.form.to_dict()
389389
else:
390390
req_body = request.get_data()
391-
except (AttributeError, RuntimeError, TypeError, BadRequest, ValueError, JSONDecodeError):
391+
except (
392+
AttributeError,
393+
RuntimeError,
394+
TypeError,
395+
BadRequest,
396+
ValueError,
397+
JSONDecodeError,
398+
xmltodict.expat.ExpatError,
399+
xmltodict.ParsingInterrupted,
400+
):
392401
log.warning("Failed to parse werkzeug request body", exc_info=True)
393402
finally:
394403
# Reset wsgi input to the beginning

ddtrace/contrib/pylons/middleware.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,14 @@ def __call__(self, environ, start_response):
110110
else: # text/plain, xml, others: take them as strings
111111
req_body = request.body.decode("UTF-8")
112112

113-
except (AttributeError, OSError, ValueError, JSONDecodeError):
113+
except (
114+
AttributeError,
115+
OSError,
116+
ValueError,
117+
JSONDecodeError,
118+
xmltodict.expat.ExpatError,
119+
xmltodict.ParsingInterrupted,
120+
):
114121
log.warning("Failed to parse request body", exc_info=True)
115122
# req_body is None
116123

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
ASM: Do not raise exceptions when failing to parse XML request body.

tests/contrib/django/test_django_appsec.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,11 @@ def test_django_request_body_plain_attack(client, test_spans, tracer):
209209
assert query == "1' or '1' = '1'"
210210

211211

212-
def test_django_request_body_json_empty(caplog, client, test_spans, tracer):
213-
with caplog.at_level(logging.WARNING), override_global_config(dict(_appsec_enabled=True)), override_env(
212+
def test_django_request_body_json_bad(caplog, client, test_spans, tracer):
213+
# Note: there is some odd interaction between hypotheses or pytest and
214+
# caplog where if you set this to WARNING the second test won't get
215+
# output unless you set all to DEBUG.
216+
with caplog.at_level(logging.DEBUG), override_global_config(dict(_appsec_enabled=True)), override_env(
214217
dict(DD_APPSEC_RULES=RULES_GOOD_PATH)
215218
):
216219
payload = '{"attack": "bad_payload",}'
@@ -227,6 +230,23 @@ def test_django_request_body_json_empty(caplog, client, test_spans, tracer):
227230
assert "Failed to parse request body" in caplog.text
228231

229232

233+
def test_django_request_body_xml_bad_logs_warning(caplog, client, test_spans, tracer):
234+
# see above about caplog
235+
with caplog.at_level(logging.DEBUG), override_global_config(dict(_appsec_enabled=True)), override_env(
236+
dict(DD_APPSEC_RULES=RULES_GOOD_PATH)
237+
):
238+
_, response = _aux_appsec_get_root_span(
239+
client,
240+
test_spans,
241+
tracer,
242+
payload="bad xml",
243+
content_type="application/xml",
244+
)
245+
246+
assert response.status_code == 200
247+
assert "Failed to parse request body" in caplog.text
248+
249+
230250
def test_django_path_params(client, test_spans, tracer):
231251
with override_global_config(dict(_appsec_enabled=True)):
232252
root_span, _ = _aux_appsec_get_root_span(

tests/contrib/flask/test_flask_appsec.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,25 @@ def test_flask_body_xml_attack(self):
231231
assert query == {"attack": "1' or '1' = '1'"}
232232

233233
def test_flask_body_json_empty_body_logs_warning(self):
234-
with self._caplog.at_level(logging.WARNING), override_global_config(dict(_appsec_enabled=True)):
234+
with self._caplog.at_level(logging.DEBUG), override_global_config(dict(_appsec_enabled=True)):
235235
self._aux_appsec_prepare_tracer()
236236
self.client.post("/", data="", content_type="application/json")
237237
assert "Failed to parse werkzeug request body" in self._caplog.text
238+
239+
def test_flask_body_json_bad_logs_warning(self):
240+
with self._caplog.at_level(logging.DEBUG), override_global_config(dict(_appsec_enabled=True)):
241+
self._aux_appsec_prepare_tracer()
242+
self.client.post("/", data="not valid json", content_type="application/json")
243+
assert "Failed to parse werkzeug request body" in self._caplog.text
244+
245+
def test_flask_body_xml_bad_logs_warning(self):
246+
with self._caplog.at_level(logging.DEBUG), override_global_config(dict(_appsec_enabled=True)):
247+
self._aux_appsec_prepare_tracer()
248+
self.client.post("/", data="bad xml", content_type="application/xml")
249+
assert "Failed to parse werkzeug request body" in self._caplog.text
250+
251+
def test_flask_body_xml_empty_logs_warning(self):
252+
with self._caplog.at_level(logging.DEBUG), override_global_config(dict(_appsec_enabled=True)):
253+
self._aux_appsec_prepare_tracer()
254+
self.client.post("/", data="", content_type="application/xml")
255+
assert "Failed to parse werkzeug request body" in self._caplog.text

0 commit comments

Comments
 (0)