Skip to content

Commit 313e56c

Browse files
chore(iast): split wrapped_view hook [backport 3.2] (#12725)
Backport 8670015 from #12706 to 3.2. `_on_wrapped_view` contains the logic to block requests for AppSec and to taint path parameters for IAST. However, since the `_on_wrapped_view` hook function is in `load_appsec`, the IAST logic doesn’t run if AppSec isn’t enabled. This PR splits that logic and creates two separate hooks. This PR is a cherry-pick of one of the commits of this PR #12639 ## Checklist - [x] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting) Co-authored-by: Alberto Vara <[email protected]>
1 parent dca500f commit 313e56c

File tree

6 files changed

+44
-33
lines changed

6 files changed

+44
-33
lines changed

ddtrace/appsec/_asm_request_context.py

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,6 @@
2525
from ddtrace.trace import Span
2626

2727

28-
if asm_config._iast_enabled:
29-
from ddtrace.appsec._iast._taint_tracking import OriginType
30-
from ddtrace.appsec._iast._taint_tracking._taint_objects import taint_pyobject
31-
32-
3328
if TYPE_CHECKING:
3429
from ddtrace.appsec._ddwaf import DDWaf_info
3530
from ddtrace.appsec._ddwaf import DDWaf_result
@@ -481,7 +476,7 @@ def _on_context_ended(ctx):
481476

482477

483478
def _on_wrapped_view(kwargs):
484-
return_value = [None, None]
479+
callback_block = None
485480
# if Appsec is enabled, we can try to block as we have the path parameters at that point
486481
if asm_config._asm_enabled and in_asm_context():
487482
log.debug("Flask WAF call for Suspicious Request Blocking on request")
@@ -490,21 +485,7 @@ def _on_wrapped_view(kwargs):
490485
call_waf_callback()
491486
if is_blocked():
492487
callback_block = get_value(_CALLBACKS, "flask_block")
493-
return_value[0] = callback_block
494-
495-
# If IAST is enabled, taint the Flask function kwargs (path parameters)
496-
497-
if asm_config._iast_enabled and kwargs:
498-
if not asm_config.is_iast_request_enabled:
499-
return return_value
500-
501-
_kwargs = {}
502-
for k, v in kwargs.items():
503-
_kwargs[k] = taint_pyobject(
504-
pyobject=v, source_name=k, source_value=v, source_origin=OriginType.PATH_PARAMETER
505-
)
506-
return_value[1] = _kwargs
507-
return return_value
488+
return callback_block
508489

509490

510491
def _on_pre_tracedrequest(ctx):
@@ -571,7 +552,7 @@ def asm_listen():
571552
trace_listen()
572553

573554
core.on("flask.finalize_request.post", _set_headers_and_response)
574-
core.on("flask.wrapped_view", _on_wrapped_view, "callback_and_args")
555+
core.on("flask.wrapped_view", _on_wrapped_view, "callbacks")
575556
core.on("flask._patched_request", _on_pre_tracedrequest)
576557
core.on("wsgi.block_decided", _on_block_decided)
577558
core.on("flask.start_response", _call_waf_first, "waf")

ddtrace/appsec/_iast/_handlers.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,21 @@ def _on_flask_patch(flask_version):
118118
_set_metric_iast_instrumented_source(OriginType.PARAMETER_NAME)
119119

120120

121+
def _iast_on_wrapped_view(kwargs):
122+
# If IAST is enabled, taint the Flask function kwargs (path parameters)
123+
if kwargs and asm_config._iast_enabled:
124+
if not asm_config.is_iast_request_enabled:
125+
return kwargs
126+
127+
_kwargs = {}
128+
for k, v in kwargs.items():
129+
_kwargs[k] = taint_pyobject(
130+
pyobject=v, source_name=k, source_value=v, source_origin=OriginType.PATH_PARAMETER
131+
)
132+
return _kwargs
133+
return kwargs
134+
135+
121136
def _on_wsgi_environ(wrapped, _instance, args, kwargs):
122137
if asm_config._iast_enabled and args and asm_config.is_iast_request_enabled:
123138
return wrapped(*((taint_structure(args[0], OriginType.HEADER_NAME, OriginType.HEADER),) + args[1:]), **kwargs)
@@ -146,9 +161,9 @@ def _on_django_patch():
146161
_set_metric_iast_instrumented_source(OriginType.PARAMETER)
147162
_set_metric_iast_instrumented_source(OriginType.PARAMETER_NAME)
148163
_set_metric_iast_instrumented_source(OriginType.BODY)
149-
164+
log.debug("[IAST] Patching Django correctly")
150165
except Exception:
151-
log.debug("Unexpected exception while patch IAST functions", exc_info=True)
166+
log.debug("[IAST] Unexpected exception while patch Django", exc_info=True)
152167

153168

154169
def _on_django_func_wrapped(fn_args, fn_kwargs, first_arg_expected_type, *_):

ddtrace/appsec/_iast/_listener.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from ddtrace.appsec._iast._handlers import _iast_on_wrapped_view
12
from ddtrace.appsec._iast._handlers import _on_asgi_finalize_response
23
from ddtrace.appsec._iast._handlers import _on_django_finalize_response_pre
34
from ddtrace.appsec._iast._handlers import _on_django_func_wrapped
@@ -24,12 +25,14 @@ def iast_listen():
2425
core.on("django.finalize_response.pre", _on_django_finalize_response_pre)
2526
core.on("django.func.wrapped", _on_django_func_wrapped)
2627
core.on("django.technical_500_response", _on_django_technical_500_response)
28+
2729
core.on("flask.patch", _on_flask_patch)
2830
core.on("flask.request_init", _on_request_init)
29-
core.on("flask._patched_request", _on_pre_tracedrequest_iast)
3031
core.on("flask.set_request_tags", _on_set_request_tags_iast)
31-
core.on("flask.finalize_request.post", _on_flask_finalize_request_post)
32+
core.on("flask.wrapped_view", _iast_on_wrapped_view, "check_kwargs")
33+
core.on("flask._patched_request", _on_pre_tracedrequest_iast)
3234
core.on("asgi.finalize_response", _on_asgi_finalize_response)
35+
core.on("flask.finalize_request.post", _on_flask_finalize_request_post)
3336
core.on("werkzeug.render_debugger_html", _on_werkzeug_render_debugger_html)
3437

3538
core.on("context.ended.wsgi.__call__", _iast_end_request)

ddtrace/contrib/internal/flask/wrappers.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,22 @@ def _wrap_call(
4545
tags=tags,
4646
) as ctx, ctx.span:
4747
if do_dispatch:
48-
result = core.dispatch_with_results("flask.wrapped_view", (kwargs,)).callback_and_args
48+
dispatch = core.dispatch_with_results("flask.wrapped_view", (kwargs,))
49+
50+
# Appsec blocks the request
51+
result = dispatch.callbacks
4952
if result:
50-
callback_block, _kwargs = result.value
53+
callback_block = result.value
5154
if callback_block:
5255
return callback_block()
56+
# IAST overrides the kwargs
57+
result = dispatch.check_kwargs
58+
if result:
59+
_kwargs = result.value
5360
if _kwargs:
5461
for k in kwargs:
5562
kwargs[k] = _kwargs[k]
63+
5664
return wrapped(*args, **kwargs)
5765

5866

tests/appsec/iast/conftest.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,13 @@ def iast_span_defaults(tracer):
138138
yield span
139139

140140

141-
# The log contains "[IAST]" but "[IAST] create_context" or "[IAST] reset_context" are valid
142-
IAST_VALID_LOG = re.compile(r"(?=.*\[IAST\] )(?!.*\[IAST\] (create_context|reset_context))")
141+
# Check if the log contains "[IAST]" to raise an error if that’s the case BUT, if the logs contains
142+
# "[IAST] create_context", "[IAST] reset_context", "[IAST] allowing", "[IAST] denying" or "[IAST] astpatch_source"
143+
# are valid logs
144+
IAST_VALID_LOG = re.compile(
145+
r"(?=.*\[IAST\] )(?!.*\[IAST\] "
146+
r"(Patching|Enabled|astpatch_source|compile_code|allowing|denying|create_context|reset_context))"
147+
)
143148

144149

145150
@pytest.fixture(autouse=True)

tests/appsec/integrations/flask_tests/test_iast_flask.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
from ddtrace.settings.asm import config as asm_config
2525
from tests.appsec.iast.iast_utils import get_line_and_hash
2626
from tests.contrib.flask import BaseFlaskTestCase
27-
from tests.utils import override_env
2827
from tests.utils import override_global_config
2928

3029

@@ -41,7 +40,7 @@ def inject_fixtures(self, caplog, telemetry_writer): # noqa: F811
4140
self._caplog = caplog
4241

4342
def setUp(self):
44-
with override_env({"_DD_IAST_USE_ROOT_SPAN": "false"}), override_global_config(
43+
with override_global_config(
4544
dict(
4645
_iast_enabled=True,
4746
_iast_deduplication_enabled=False,
@@ -54,7 +53,7 @@ def setUp(self):
5453
patch_xss_injection()
5554
patch_json()
5655
super(FlaskAppSecIASTEnabledTestCase, self).setUp()
57-
self.tracer._configure(api_version="v0.4", appsec_enabled=True, iast_enabled=True)
56+
self.tracer._configure(api_version="v0.4", iast_enabled=True)
5857
oce.reconfigure()
5958

6059
@pytest.mark.skipif(not asm_config._iast_supported, reason="Python version not supported by IAST")

0 commit comments

Comments
 (0)