Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions ddtrace/appsec/_iast/taint_sinks/code_injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ def get_version() -> Text:

_IS_PATCHED = False

# Upper bound on wrapper frames to skip when locating the caller of eval.
_MAX_WRAPPER_FRAMES = 8


def patch():
global _IS_PATCHED
Expand Down Expand Up @@ -53,6 +56,23 @@ class CodeInjection(VulnerabilityBase):
secure_mark = VulnerabilityType.CODE_INJECTION


def _resolve_caller_frame(frame):
"""Walk past the wrapping machinery so we land on the frame that called eval.

wrapt's pure-Python FunctionWrapper adds a frame that its C extension does not, so a
fixed f_back depth resolves to the wrong scope whenever the C extension is unavailable.
"""
candidate = frame
for _ in range(_MAX_WRAPPER_FRAMES):
if candidate is None:
break
module_name = candidate.f_globals.get("__name__") or ""
if module_name != "wrapt" and not module_name.startswith("wrapt."):
return candidate
candidate = candidate.f_back
return frame


def _iast_coi(wrapped, instance, args, kwargs):
if len(args) >= 1:
_iast_report_code_injection(args[0])
Expand Down Expand Up @@ -81,7 +101,8 @@ def _iast_coi(wrapped, instance, args, kwargs):
else:
frames = inspect.currentframe()
if frames is not None:
caller_frame = frames.f_back
caller_frame = _resolve_caller_frame(frames.f_back)
if caller_frame is not None:
func_globals = caller_frame.f_globals

if len(args) > 2:
Expand All @@ -92,7 +113,7 @@ def _iast_coi(wrapped, instance, args, kwargs):
if caller_frame is None:
frames = inspect.currentframe()
if frames is not None:
caller_frame = frames.f_back
caller_frame = _resolve_caller_frame(frames.f_back)
if caller_frame is not None:
func_locals = caller_frame.f_locals
func_locals_copy_to_check = func_locals.copy() if func_locals else None
Expand Down
6 changes: 4 additions & 2 deletions ddtrace/internal/settings/asm.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,10 @@ class ASMConfig(DDConfig):
_bypass_instrumentation_for_waf = False
_is_testing_instrumentation_for_waf = False

# IAST supported on python 3.6 to 3.13 and never on windows
_iast_supported: bool = ((3, 6, 0) <= sys.version_info < (3, 15, 0)) and not (
# AIDEV-NOTE: Only runtime gate for IAST version support; the native extensions are not
# version-gated in setup.py. Keep the upper bound in sync with requires-python.
# IAST supported on python 3.6 to 3.15 and never on windows
_iast_supported: bool = ((3, 6, 0) <= sys.version_info < (3, 16, 0)) and not (
sys.platform.startswith("win") or sys.platform.startswith("cygwin")
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
fixes:
- |
Code Security (IAST): Fixes a ``NameError`` raised from application code that calls ``eval()``
when the ``wrapt`` C extension is unavailable and its pure-Python implementation is used, which
caused Code Security to resolve the wrong caller scope.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
features:
- |
Code Security (IAST): Adds support for Python 3.15. Setting ``DD_IAST_ENABLED=true`` now
enables Code Security on Python 3.15; previously it was silently ignored on that version.
4 changes: 3 additions & 1 deletion tests/appsec/iast/aspects/test_slice_aspect_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ def test_string_slice_2(input_str, start_pos, end_pos, step, expected_result, ta
if not tainted:
with pytest.raises(TypeError) as excinfo:
mod.do_slice_2(input_str, start_pos, end_pos, step) # pylint: disable=no-member
assert "slice indices must be integers or None or have an __index__ method" in str(excinfo.value)
# Python 3.15 dropped the "or None" clause from this message.
assert "slice indices must be integers or" in str(excinfo.value)
assert "have an __index__ method" in str(excinfo.value)
else:
result = mod.do_slice_2(input_str, start_pos, end_pos, step) # pylint: disable=no-member
assert result == expected_result
Expand Down
Loading