Skip to content

Commit c1f3e99

Browse files
gnufedeavara1986
andauthored
fix(iast): prepare aspect for unexpected args (#7491) [backport 1.20] (#7573)
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 fcf5dbb commit c1f3e99

File tree

14 files changed

+415
-46
lines changed

14 files changed

+415
-46
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: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from _ast import Expr
44
from _ast import ImportFrom
55
import ast
6+
import copy
67
import sys
78
from typing import TYPE_CHECKING
89

@@ -155,6 +156,34 @@ def _is_string_format_with_literals(self, call_node):
155156
and all(map(lambda x: self._is_node_constant_or_binop(x.value), call_node.keywords))
156157
)
157158

159+
def _get_function_name(self, call_node, is_function): # type: (ast.Call, bool) -> str
160+
if is_function:
161+
return call_node.func.id # type: ignore[attr-defined]
162+
# If the call is to a method
163+
elif type(call_node.func) == ast.Name:
164+
return call_node.func.id
165+
166+
return call_node.func.attr # type: ignore[attr-defined]
167+
168+
def _add_original_function_as_arg(self, call_node, is_function): # type: (ast.Call, bool) -> Any
169+
"""
170+
Creates the arguments for the original function
171+
"""
172+
function_name = self._get_function_name(call_node, is_function)
173+
function_name_arg = (
174+
self._name_node(call_node, function_name, ctx=ast.Load()) if is_function else copy.copy(call_node.func)
175+
)
176+
177+
# Arguments for stack info change from:
178+
# my_function(self, *args, **kwargs)
179+
# to:
180+
# _add_original_function_as_arg(function_name=my_function, self, *args, **kwargs)
181+
new_args = [
182+
function_name_arg,
183+
] + call_node.args
184+
185+
return new_args
186+
158187
def _node(self, type_, pos_from_node, **kwargs):
159188
# type: (Any, Any, Any) -> Any
160189
"""
@@ -247,6 +276,16 @@ def _none_constant(self, from_node, ctx=ast.Load()): # noqa: B008
247276
kind=None,
248277
)
249278

279+
def _int_constant(self, from_node, value):
280+
return ast.Constant(
281+
lineno=from_node.lineno,
282+
col_offset=from_node.col_offset,
283+
end_lineno=getattr(from_node, "end_lineno", from_node.lineno),
284+
end_col_offset=from_node.col_offset + 1,
285+
value=value,
286+
kind=None,
287+
)
288+
250289
def _call_node(self, from_node, func, args): # type: (Any, Any, List[Any]) -> Any
251290
return self._node(ast.Call, from_node, func=func, args=args, keywords=[])
252291

@@ -319,6 +358,11 @@ def visit_Call(self, call_node): # type: (ast.Call) -> Any
319358
func_name_node = func_member.id
320359
aspect = self._aspect_functions.get(func_name_node)
321360
if aspect:
361+
# Send 0 as flag_added_args value
362+
call_node.args.insert(0, self._int_constant(call_node, 0))
363+
# Insert original function name as first parameter
364+
call_node.args = self._add_original_function_as_arg(call_node, True)
365+
# Substitute function call
322366
call_node.func = self._attr_node(call_node, aspect)
323367
self.ast_modified = call_modified = True
324368
else:
@@ -346,6 +390,11 @@ def visit_Call(self, call_node): # type: (ast.Call) -> Any
346390
# Move the Attribute.value to 'args'
347391
new_arg = func_member.value
348392
call_node.args.insert(0, new_arg)
393+
# Send 1 as flag_added_args value
394+
call_node.args.insert(0, self._int_constant(call_node, 1))
395+
396+
# Insert original method as first parameter (a.b.c.method)
397+
call_node.args = self._add_original_function_as_arg(call_node, False)
349398

350399
# Create a new Name node for the replacement and set it as node.func
351400
call_node.func = self._attr_node(call_node, aspect)
@@ -354,6 +403,8 @@ def visit_Call(self, call_node): # type: (ast.Call) -> Any
354403
elif hasattr(func_member.value, "id") or hasattr(func_member.value, "attr"):
355404
aspect = self._aspect_modules.get(method_name, None)
356405
if aspect:
406+
# Send 0 as flag_added_args value
407+
call_node.args.insert(0, self._int_constant(call_node, 0))
357408
# Move the Function to 'args'
358409
call_node.args.insert(0, call_node.func)
359410

0 commit comments

Comments
 (0)