Skip to content

Commit dbddf76

Browse files
brettlangdongnufedechristophe-papazian
authored
chore(asm): fix ddwaf errors found in 1.8.0rc1 [backport #4975 to 1.8] (#4991)
## Description - Avoid any errors or exceptions raised while running the WAF to interrupt the request - Add test coverage and regression tests ## Checklist - [ ] Followed the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/contributing.html#Release-Note-Guidelines) when writing a release note. - [ ] Add additional sections for `feat` and `fix` pull requests. - [ ] [Library documentation](https://github.com/DataDog/dd-trace-py/tree/1.x/docs) and/or [Datadog's documentation site](https://github.com/DataDog/documentation/) is updated. Link to doc PR in description. ## Reviewer Checklist - [ ] Title is accurate. - [ ] Description motivates each change. - [ ] No unnecessary changes were introduced in this PR. - [ ] Avoid breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [ ] Tests provided or description of manual testing performed is included in the code or PR. - [ ] Release note has been added and follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/contributing.html#Release-Note-Guidelines), or else `changelog/no-changelog` label added. - [ ] All relevant GitHub issues are correctly linked. - [ ] Change contains telemetry where appropriate (logs, metrics, etc.). - [ ] Telemetry is meaningful, actionable and does not have the potential to leak sensitive data. --------- Co-authored-by: Christophe Papazian <[email protected]> Co-authored-by: Federico Mon <[email protected]> Co-authored-by: Christophe Papazian <[email protected]>
1 parent 4fffa77 commit dbddf76

File tree

4 files changed

+170
-32
lines changed

4 files changed

+170
-32
lines changed

ddtrace/appsec/ddwaf/__init__.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@
2424
from .ddwaf_types import ddwaf_get_version
2525
from .ddwaf_types import ddwaf_init
2626
from .ddwaf_types import ddwaf_object
27+
from .ddwaf_types import ddwaf_result
2728
from .ddwaf_types import ddwaf_result_free
2829
from .ddwaf_types import ddwaf_ruleset_info
30+
from .ddwaf_types import ddwaf_run
2931
from .ddwaf_types import ddwaf_update_rule_data
3032
from .ddwaf_types import py_ddwaf_required_addresses
31-
from .ddwaf_types import py_ddwaf_run
3233

3334
_DDWAF_LOADED = True
3435
except OSError:
@@ -109,18 +110,27 @@ def run(
109110

110111
ctx = ddwaf_context_init(self._handle)
111112
if ctx == 0:
112-
raise RuntimeError
113+
LOGGER.error("DDWaf failure to create the context")
114+
return DDWaf_result(None, [], 0, (time.time() - start) * 1e6)
115+
113116
try:
117+
result = ddwaf_result()
114118
wrapper = ddwaf_object(data)
115-
error, result = py_ddwaf_run(ctx, wrapper, timeout_ms * 1000)
116-
return DDWaf_result(
117-
result.data.decode("UTF-8") if result.data else None,
118-
[result.actions.array[i].decode("UTF-8") for i in range(result.actions.size)],
119-
result.total_runtime / 1e3,
120-
(time.time() - start) * 1e6,
121-
)
119+
error = ddwaf_run(ctx, wrapper, ctypes.byref(result), timeout_ms * 1000)
120+
if error:
121+
LOGGER.warning("DDWAF error: %d", error)
122+
try:
123+
return DDWaf_result(
124+
result.data.decode("UTF-8", errors="ignore")
125+
if hasattr(result, "data") and result.data
126+
else None,
127+
[result.actions.array[i].decode("UTF-8", errors="ignore") for i in range(result.actions.size)],
128+
result.total_runtime / 1e3,
129+
(time.time() - start) * 1e6,
130+
)
131+
finally:
132+
ddwaf_result_free(ctypes.byref(result))
122133
finally:
123-
ddwaf_result_free(ctypes.byref(result))
124134
ddwaf_context_destroy(ctx)
125135

126136
def __dealloc__(self):

ddtrace/appsec/ddwaf/ddwaf_types.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,7 @@ class ddwaf_object(ctypes.Structure):
108108

109109
def __init__(self, struct=None):
110110
# type: (ddwaf_object, DDWafRulesType|None) -> None
111-
if struct is None:
112-
ddwaf_object_invalid(self)
113-
elif isinstance(struct, (int, long)):
111+
if isinstance(struct, (int, long)):
114112
ddwaf_object_signed(self, struct)
115113
elif isinstance(struct, unicode):
116114
ddwaf_object_string(self, struct.encode("UTF-8", errors="ignore"))
@@ -130,15 +128,18 @@ def __init__(self, struct=None):
130128
map_o = ddwaf_object_map(self)
131129
assert map_o
132130
# order is unspecified and could lead to problems if max_objects is reached
133-
for (key, val) in struct.items():
131+
for key, val in struct.items():
134132
if not isinstance(key, (bytes, unicode)): # discards non string keys
135133
continue
136134
res_key = key.encode("UTF-8", errors="ignore") if isinstance(key, unicode) else key
137135
obj = ddwaf_object(val)
138136
if obj.type: # discards invalid objects
139137
assert ddwaf_object_map_add(map_o, res_key, obj)
140138
else:
141-
raise TypeError("ddwaf_object : unknown type in structure. " + repr(type(struct)))
139+
if struct is not None:
140+
log.warning("DDWAF object init called with unknown data structure: %s", repr(type(struct)))
141+
142+
ddwaf_object_invalid(self)
142143

143144
@property
144145
def struct(self):
@@ -151,17 +152,18 @@ def struct(self):
151152
if self.type == DDWAF_OBJ_TYPE.DDWAF_OBJ_UNSIGNED:
152153
return self.value.uintValue
153154
if self.type == DDWAF_OBJ_TYPE.DDWAF_OBJ_STRING:
154-
return self.value.stringValue.decode("UTF-8")
155+
return self.value.stringValue.decode("UTF-8", errors="ignore")
155156
if self.type == DDWAF_OBJ_TYPE.DDWAF_OBJ_ARRAY:
156157
return [self.value.array[i].struct for i in range(self.nbEntries)]
157158
if self.type == DDWAF_OBJ_TYPE.DDWAF_OBJ_MAP:
158159
return {
159-
self.value.array[i].parameterName.decode("UTF-8"): self.value.array[i].struct
160+
self.value.array[i].parameterName.decode("UTF-8", errors="ignore"): self.value.array[i].struct
160161
for i in range(self.nbEntries)
161162
}
162163
if self.type == DDWAF_OBJ_TYPE.DDWAF_OBJ_BOOL:
163164
return self.value.boolean
164-
raise ValueError("ddwaf_object: unknown object")
165+
log.warning("ddwaf_object struct: unknown object type: %s", repr(type(self.type)))
166+
return None
165167

166168
def __repr__(self):
167169
return repr(self.struct)
@@ -373,14 +375,6 @@ def py_ddwaf_required_rule_data_ids(handle):
373375
("ddwaf_run", ddwaf), ((1, "context"), (1, "data"), (1, "result"), (1, "timeout"))
374376
)
375377

376-
377-
def py_ddwaf_run(context, object_p, timeout):
378-
# type : (...) -> tuple[int, ddwaf_result]
379-
res = ddwaf_result()
380-
err = ddwaf_run(context, object_p, ctypes.byref(res), timeout)
381-
return err, res
382-
383-
384378
ddwaf_context_destroy = ctypes.CFUNCTYPE(None, ddwaf_context)(
385379
("ddwaf_context_destroy", ddwaf),
386380
((1, "context"),),

ddtrace/appsec/processor.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@
3434
from ddtrace.internal.rate_limiter import RateLimiter
3535

3636

37+
try:
38+
from json.decoder import JSONDecodeError
39+
except ImportError:
40+
# handling python 2.X import error
41+
JSONDecodeError = ValueError # type: ignore
42+
3743
if TYPE_CHECKING: # pragma: no cover
3844
from typing import Dict
3945

@@ -180,7 +186,7 @@ def __attrs_post_init__(self):
180186
# TODO: try to log reasons
181187
log.error("[DDAS-0001-03] AppSec could not read the rule file %s.", self.rules)
182188
raise
183-
except json.decoder.JSONDecodeError:
189+
except JSONDecodeError:
184190
log.error(
185191
"[DDAS-0001-03] AppSec could not read the rule file %s. Reason: invalid JSON file", self.rules
186192
)
@@ -270,7 +276,13 @@ def on_span_finish(self, span):
270276
data[_Addresses.SERVER_REQUEST_BODY] = body
271277

272278
log.debug("[DDAS-001-00] Executing AppSec In-App WAF with parameters: %s", data)
273-
ddwaf_result = self._ddwaf.run(data, self._waf_timeout) # res is a serialized json
279+
ddwaf_result = None
280+
try:
281+
ddwaf_result = self._ddwaf.run(data, self._waf_timeout) # res is a serialized json
282+
except OSError:
283+
log.warning("Error executing Appsec In-App WAF: ", exc_info=True)
284+
except Exception as e:
285+
log.warning("Error executing Appsec In-App WAF: %s", repr(e))
274286

275287
try:
276288
info = self._ddwaf.info
@@ -281,13 +293,15 @@ def on_span_finish(self, span):
281293

282294
span.set_metric(APPSEC_EVENT_RULE_LOADED, info.loaded)
283295
span.set_metric(APPSEC_EVENT_RULE_ERROR_COUNT, info.failed)
284-
span.set_metric(APPSEC_WAF_DURATION, ddwaf_result.runtime)
285-
span.set_metric(APPSEC_WAF_DURATION_EXT, ddwaf_result.total_runtime)
286-
except (json.decoder.JSONDecodeError, ValueError):
296+
if ddwaf_result:
297+
span.set_metric(APPSEC_WAF_DURATION, ddwaf_result.runtime)
298+
span.set_metric(APPSEC_WAF_DURATION_EXT, ddwaf_result.total_runtime)
299+
except JSONDecodeError:
287300
log.warning("Error parsing data AppSec In-App WAF metrics report")
288301
except Exception:
289302
log.warning("Error executing AppSec In-App WAF metrics report: %s", exc_info=True)
290-
if ddwaf_result.data is not None:
303+
304+
if ddwaf_result and ddwaf_result.data is not None:
291305
# We run the rate limiter only if there is an attack, its goal is to limit the number of collected asm
292306
# events
293307
allowed = self._rate_limiter.is_allowed(span.start_ns)

tests/appsec/test_processor.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import json
2+
import logging
23
import os.path
34

5+
import mock
46
import pytest
57
from six import ensure_binary
68

@@ -20,6 +22,12 @@
2022
from tests.utils import snapshot
2123

2224

25+
try:
26+
from json.decoder import JSONDecodeError
27+
except ImportError:
28+
# handling python 2.X import error
29+
JSONDecodeError = ValueError # type: ignore
30+
2331
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
2432
RULES_GOOD_PATH = os.path.join(ROOT_DIR, "rules-good.json")
2533
RULES_BAD_PATH = os.path.join(ROOT_DIR, "rules-bad.json")
@@ -423,3 +431,115 @@ def test_ddwaf_info_with_3_errors():
423431
assert info.loaded == 1
424432
assert info.failed == 2
425433
assert info.errors == {"missing key 'name'": ["crs-942-100", "crs-913-120"]}
434+
435+
436+
def test_ddwaf_info_with_json_decode_errors(tracer_appsec, caplog):
437+
tracer = tracer_appsec
438+
config = Config()
439+
config.http_tag_query_string = True
440+
441+
with caplog.at_level(logging.WARNING), override_env(dict(DD_TRACE_CLIENT_IP_HEADER_DISABLED="False")), mock.patch(
442+
"ddtrace.appsec.processor.json.dumps", side_effect=JSONDecodeError("error", "error", 0)
443+
), mock.patch.object(DDWaf, "info"):
444+
with tracer.trace("test", span_type=SpanTypes.WEB) as span:
445+
set_http_meta(
446+
span,
447+
config,
448+
method="PATCH",
449+
url=u"http://localhost/api/unstable/role_requests/dab1e9ae-9d99-11ed-bfdf-da7ad0900000?_authentication_token=2b0297348221f294de3a047e2ecf1235abb866b6", # noqa: E501
450+
status_code="200",
451+
raw_uri=u"http://localhost/api/unstable/role_requests/dab1e9ae-9d99-11ed-bfdf-da7ad0900000?_authentication_token=2b0297348221f294de3a047e2ecf1235abb866b6", # noqa: E501
452+
request_headers={
453+
"host": u"localhost",
454+
"user-agent": "aa",
455+
"content-length": u"73",
456+
},
457+
response_headers={
458+
"content-length": "501",
459+
"x-ratelimit-remaining": "363",
460+
"x-ratelimit-name": "role_api",
461+
"x-ratelimit-limit": "500",
462+
"x-ratelimit-period": "60",
463+
"content-type": "application/json",
464+
"x-ratelimit-reset": "16",
465+
},
466+
request_body={"_authentication_token": u"2b0297348221f294de3a047e2ecf1235abb866b6"},
467+
)
468+
469+
assert "Error parsing data AppSec In-App WAF metrics report" in caplog.text
470+
471+
472+
def test_ddwaf_run_contained_typeerror(tracer_appsec, caplog):
473+
tracer = tracer_appsec
474+
475+
config = Config()
476+
config.http_tag_query_string = True
477+
478+
with caplog.at_level(logging.WARNING), mock.patch(
479+
"ddtrace.appsec.ddwaf.ddwaf_run", side_effect=TypeError("expected c_long instead of int")
480+
), override_env(dict(DD_TRACE_CLIENT_IP_HEADER_DISABLED="False")):
481+
with tracer.trace("test", span_type=SpanTypes.WEB) as span:
482+
set_http_meta(
483+
span,
484+
config,
485+
method="PATCH",
486+
url=u"http://localhost/api/unstable/role_requests/dab1e9ae-9d99-11ed-bfdf-da7ad0900000?_authentication_token=2b0297348221f294de3a047e2ecf1235abb866b6", # noqa: E501
487+
status_code="200",
488+
raw_uri=u"http://localhost/api/unstable/role_requests/dab1e9ae-9d99-11ed-bfdf-da7ad0900000?_authentication_token=2b0297348221f294de3a047e2ecf1235abb866b6", # noqa: E501
489+
request_headers={
490+
"host": u"localhost",
491+
"user-agent": "aa",
492+
"content-length": u"73",
493+
},
494+
response_headers={
495+
"content-length": "501",
496+
"x-ratelimit-remaining": "363",
497+
"x-ratelimit-name": "role_api",
498+
"x-ratelimit-limit": "500",
499+
"x-ratelimit-period": "60",
500+
"content-type": "application/json",
501+
"x-ratelimit-reset": "16",
502+
},
503+
request_body={"_authentication_token": u"2b0297348221f294de3a047e2ecf1235abb866b6"},
504+
)
505+
506+
assert span.get_tag(APPSEC_JSON) is None
507+
assert "Error executing Appsec In-App WAF: TypeError('expected c_long instead of int'" in caplog.text
508+
509+
510+
def test_ddwaf_run_contained_oserror(tracer_appsec, caplog):
511+
tracer = tracer_appsec
512+
513+
config = Config()
514+
config.http_tag_query_string = True
515+
516+
with caplog.at_level(logging.WARNING), mock.patch(
517+
"ddtrace.appsec.ddwaf.ddwaf_run", side_effect=OSError("ddwaf run failed")
518+
), override_env(dict(DD_TRACE_CLIENT_IP_HEADER_DISABLED="False")):
519+
with tracer.trace("test", span_type=SpanTypes.WEB) as span:
520+
set_http_meta(
521+
span,
522+
config,
523+
method="PATCH",
524+
url=u"http://localhost/api/unstable/role_requests/dab1e9ae-9d99-11ed-bfdf-da7ad0900000?_authentication_token=2b0297348221f294de3a047e2ecf1235abb866b6", # noqa: E501
525+
status_code="200",
526+
raw_uri=u"http://localhost/api/unstable/role_requests/dab1e9ae-9d99-11ed-bfdf-da7ad0900000?_authentication_token=2b0297348221f294de3a047e2ecf1235abb866b6", # noqa: E501
527+
request_headers={
528+
"host": u"localhost",
529+
"user-agent": "aa",
530+
"content-length": u"73",
531+
},
532+
response_headers={
533+
"content-length": "501",
534+
"x-ratelimit-remaining": "363",
535+
"x-ratelimit-name": "role_api",
536+
"x-ratelimit-limit": "500",
537+
"x-ratelimit-period": "60",
538+
"content-type": "application/json",
539+
"x-ratelimit-reset": "16",
540+
},
541+
request_body={"_authentication_token": u"2b0297348221f294de3a047e2ecf1235abb866b6"},
542+
)
543+
544+
assert span.get_tag(APPSEC_JSON) is None
545+
assert "Error executing Appsec In-App WAF: \nTraceback (" in caplog.text

0 commit comments

Comments
 (0)