Skip to content

Commit 5290d5a

Browse files
fix(asm): capture XML parsing errors on body parse (#4559) (#4575)
* 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 eeafbd8 commit 5290d5a

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
@@ -273,6 +273,8 @@ def _extract_body(request):
273273
OSError,
274274
ValueError,
275275
JSONDecodeError,
276+
xmltodict.expat.ExpatError,
277+
xmltodict.ParsingInterrupted,
276278
):
277279
log.warning("Failed to parse request body")
278280
# req_body is None

ddtrace/contrib/flask/patch.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,16 @@ def traced_wsgi_app(pin, wrapped, instance, args, kwargs):
385385
req_body = request.form.to_dict()
386386
else:
387387
req_body = request.get_data()
388-
except (AttributeError, RuntimeError, TypeError, BadRequest, ValueError, JSONDecodeError):
388+
except (
389+
AttributeError,
390+
RuntimeError,
391+
TypeError,
392+
BadRequest,
393+
ValueError,
394+
JSONDecodeError,
395+
xmltodict.expat.ExpatError,
396+
xmltodict.ParsingInterrupted,
397+
):
389398
log.warning("Failed to parse werkzeug request body", exc_info=True)
390399
finally:
391400
# 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
@@ -207,8 +207,11 @@ def test_django_request_body_plain_attack(client, test_spans, tracer):
207207
assert query == "1' or '1' = '1'"
208208

209209

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

227230

231+
def test_django_request_body_xml_bad_logs_warning(caplog, client, test_spans, tracer):
232+
# see above about caplog
233+
with caplog.at_level(logging.DEBUG), override_global_config(dict(_appsec_enabled=True)), override_env(
234+
dict(DD_APPSEC_RULES=RULES_GOOD_PATH)
235+
):
236+
_, response = _aux_appsec_get_root_span(
237+
client,
238+
test_spans,
239+
tracer,
240+
payload="bad xml",
241+
content_type="application/xml",
242+
)
243+
244+
assert response.status_code == 200
245+
assert "Failed to parse request body" in caplog.text
246+
247+
228248
def test_django_path_params(client, test_spans, tracer):
229249
with override_global_config(dict(_appsec_enabled=True)):
230250
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
@@ -229,7 +229,25 @@ def test_flask_body_xml_attack(self):
229229
assert query == {"attack": "1' or '1' = '1'"}
230230

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

0 commit comments

Comments
 (0)