Skip to content

Commit 2bde90e

Browse files
gnufedeavara1986
andauthored
fix(iast): prepare aspect for unexpected args (#7491) [backport 2.0] (#7574)
IAST: This PR addresses two interconnected issues in the AST patching process that replaces code with aspects: 1. Resolves a bug that occurs during AST patching, where a custom function or method call could be replaced by one of our aspects. In situations where the function has a different number of arguments than our aspects, a runtime ``TypeError`` occurs. This is fixed by ensuring that all aspects now receive ``(*args, **kwargs)`` and pass them appropriately to the original custom function or method. 2. Fixes another bug in the AST patching process, where the module of a function is incorrectly passed as the first argument to the aspect. In cases where it's a custom function, our logic inadvertently passes the module received as the first argument to the original function. The solution involves introducing a flag to the aspect's arguments, indicating the additional arguments added during patching. This allows us to remove them before calling the original function or method. - [x] Change(s) are motivated and described in the PR description. - [x] Testing strategy is described if automated tests are not included in the PR. - [x] Risk is outlined (performance impact, potential for breakage, maintainability, etc). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] [Library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) are followed. If no release note is required, add label `changelog/no-changelog`. - [x] Documentation is included (in-code, generated user docs, [public corp docs](https://github.com/DataDog/documentation/)). - [x] Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) - [x] Title is accurate. - [x] No unnecessary changes are introduced. - [x] Description motivates each change. - [x] Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [x] Testing strategy adequately addresses listed risk(s). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] Release note makes sense to a user of the library. - [x] Reviewer has explicitly acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment. - [x] 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) - [x] If this PR touches code that signs or publishes builds or packages, or handles credentials of any kind, I've requested a review from `@DataDog/security-design-and-guidance`. - [x] This PR doesn't touch any of that. --------- Co-authored-by: Alberto Vara <[email protected]> (cherry picked from commit d5caa6c)
1 parent 8860fd6 commit 2bde90e

File tree

15 files changed

+424
-66
lines changed

15 files changed

+424
-66
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,7 @@ ddtrace/appsec/_iast/_taint_tracking/CMakeFiles/*
144144

145145
# CircleCI generated config
146146
.circleci/config.gen.yml
147+
148+
# Automatically generated fixtures
149+
tests/appsec/iast/fixtures/aspects/callers.py
150+
tests/appsec/iast/fixtures/aspects/unpatched_callers.py

benchmarks/appsec_iast_propagation/scenario.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def aspect_function(internal_loop, tainted):
3838
value = ""
3939
res = value
4040
for _ in range(internal_loop):
41-
res = add_aspect(res, join_aspect("_", (tainted, "_", tainted)))
41+
res = add_aspect(res, join_aspect(str.join, 1, "_", (tainted, "_", tainted)))
4242
value = res
4343
res = add_aspect(res, tainted)
4444
value = res

ddtrace/appsec/_iast/_ast/visitor.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
CODE_TYPE_DD = "datadog"
2626
CODE_TYPE_SITE_PACKAGES = "site_packages"
2727
CODE_TYPE_STDLIB = "stdlib"
28-
TAINT_SINK_FUNCTION_REPLACEMENT = "ddtrace_taint_sinks.ast_funcion"
28+
TAINT_SINK_FUNCTION_REPLACEMENT = "ddtrace_taint_sinks.ast_function"
2929

3030

3131
class AstVisitor(ast.NodeTransformer):
@@ -326,6 +326,16 @@ def _none_constant(self, from_node, ctx=ast.Load()): # noqa: B008
326326
kind=None,
327327
)
328328

329+
def _int_constant(self, from_node, value):
330+
return ast.Constant(
331+
lineno=from_node.lineno,
332+
col_offset=from_node.col_offset,
333+
end_lineno=getattr(from_node, "end_lineno", from_node.lineno),
334+
end_col_offset=from_node.col_offset + 1,
335+
value=value,
336+
kind=None,
337+
)
338+
329339
def _call_node(self, from_node, func, args): # type: (Any, Any, List[Any]) -> Any
330340
return self._node(ast.Call, from_node, func=func, args=args, keywords=[])
331341

@@ -398,6 +408,11 @@ def visit_Call(self, call_node): # type: (ast.Call) -> Any
398408
func_name_node = func_member.id
399409
aspect = self._aspect_functions.get(func_name_node)
400410
if aspect:
411+
# Send 0 as flag_added_args value
412+
call_node.args.insert(0, self._int_constant(call_node, 0))
413+
# Insert original function name as first parameter
414+
call_node.args = self._add_original_function_as_arg(call_node, True)
415+
# Substitute function call
401416
call_node.func = self._attr_node(call_node, aspect)
402417
self.ast_modified = call_modified = True
403418
else:
@@ -425,6 +440,11 @@ def visit_Call(self, call_node): # type: (ast.Call) -> Any
425440
# Move the Attribute.value to 'args'
426441
new_arg = func_member.value
427442
call_node.args.insert(0, new_arg)
443+
# Send 1 as flag_added_args value
444+
call_node.args.insert(0, self._int_constant(call_node, 1))
445+
446+
# Insert original method as first parameter (a.b.c.method)
447+
call_node.args = self._add_original_function_as_arg(call_node, False)
428448

429449
# Create a new Name node for the replacement and set it as node.func
430450
call_node.func = self._attr_node(call_node, aspect)
@@ -433,6 +453,8 @@ def visit_Call(self, call_node): # type: (ast.Call) -> Any
433453
elif hasattr(func_member.value, "id") or hasattr(func_member.value, "attr"):
434454
aspect = self._aspect_modules.get(method_name, None)
435455
if aspect:
456+
# Send 0 as flag_added_args value
457+
call_node.args.insert(0, self._int_constant(call_node, 0))
436458
# Move the Function to 'args'
437459
call_node.args.insert(0, call_node.func)
438460

@@ -445,6 +467,8 @@ def visit_Call(self, call_node): # type: (ast.Call) -> Any
445467
if isinstance(call_node.func, ast.Name):
446468
aspect = self._should_replace_with_taint_sink(call_node, True)
447469
if aspect:
470+
# Send 0 as flag_added_args value
471+
call_node.args.insert(0, self._int_constant(call_node, 0))
448472
call_node.args = self._add_original_function_as_arg(call_node, False)
449473
call_node.func = self._attr_node(call_node, TAINT_SINK_FUNCTION_REPLACEMENT)
450474
self.ast_modified = call_modified = True
@@ -453,6 +477,8 @@ def visit_Call(self, call_node): # type: (ast.Call) -> Any
453477
elif isinstance(call_node.func, ast.Attribute):
454478
aspect = self._should_replace_with_taint_sink(call_node, False)
455479
if aspect:
480+
# Send 0 as flag_added_args value
481+
call_node.args.insert(0, self._int_constant(call_node, 0))
456482
# Create a new Name node for the replacement and set it as node.func
457483
call_node.args = self._add_original_function_as_arg(call_node, False)
458484
call_node.func = self._attr_node(call_node, TAINT_SINK_FUNCTION_REPLACEMENT)

0 commit comments

Comments
 (0)