Skip to content

Commit f97d65d

Browse files
fix(iast): ast patching side effect [backport 2.1] (#7653)
Backport 601fd6a from #7638 to 2.1. IAST: This fix resolves an issue where, at AST patching to replace code with IAST aspects, the original function/method was passed as an extra parameter for accurate patching (i.e. check the function inside the aspect), and this strategy lead to unintentionally triggers side effects in methods obtained from an expression (like ``decode`` in ``file.read(n).decode()``), resulting in unexpected multiple calls to the expression (``file.read(n)`` in the example). ## Checklist - [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)) ## Reviewer Checklist - [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: Federico Mon <[email protected]>
1 parent b5f6313 commit f97d65d

File tree

9 files changed

+342
-73
lines changed

9 files changed

+342
-73
lines changed

ddtrace/appsec/_iast/_ast/visitor.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,8 +470,9 @@ def visit_Call(self, call_node): # type: (ast.Call) -> Any
470470
# Send 1 as flag_added_args value
471471
call_node.args.insert(0, self._int_constant(call_node, 1))
472472

473-
# Insert original method as first parameter (a.b.c.method)
474-
call_node.args = self._add_original_function_as_arg(call_node, False)
473+
# Insert None as first parameter instead of a.b.c.method
474+
# to avoid unexpected side effects such as a.b.read(4).method
475+
call_node.args.insert(0, self._none_constant(call_node))
475476

476477
# Create a new Name node for the replacement and set it as node.func
477478
call_node.func = self._attr_node(call_node, aspect)

ddtrace/appsec/_iast/_taint_tracking/aspects.py

Lines changed: 101 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import TYPE_CHECKING
77
from typing import Any
88
from typing import Callable
9+
from typing import Optional
910

1011
from ddtrace.internal.compat import iteritems
1112

@@ -52,13 +53,15 @@ def add_aspect(op1, op2):
5253

5354

5455
def str_aspect(orig_function, flag_added_args, *args, **kwargs):
55-
# type: (Callable, int, Any, Any) -> str
56-
if orig_function != builtin_str:
57-
if flag_added_args > 0:
58-
args = args[flag_added_args:]
59-
return orig_function(*args, **kwargs)
60-
61-
result = builtin_str(*args, **kwargs)
56+
# type: (Optional[Callable], int, Any, Any) -> str
57+
if orig_function:
58+
if orig_function != builtin_str:
59+
if flag_added_args > 0:
60+
args = args[flag_added_args:]
61+
return orig_function(*args, **kwargs)
62+
result = builtin_str(*args, **kwargs)
63+
else:
64+
result = args[0].str(*args[1:], **kwargs)
6265

6366
if isinstance(args[0], TEXT_TYPES) and is_pyobject_tainted(args[0]):
6467
try:
@@ -76,13 +79,16 @@ def str_aspect(orig_function, flag_added_args, *args, **kwargs):
7679

7780

7881
def bytes_aspect(orig_function, flag_added_args, *args, **kwargs):
79-
# type: (Callable, int, Any, Any) -> bytes
80-
if orig_function != builtin_bytes:
81-
if flag_added_args > 0:
82-
args = args[flag_added_args:]
83-
return orig_function(*args, **kwargs)
82+
# type: (Optional[Callable], int, Any, Any) -> bytes
83+
if orig_function:
84+
if orig_function != builtin_bytes:
85+
if flag_added_args > 0:
86+
args = args[flag_added_args:]
87+
return orig_function(*args, **kwargs)
88+
result = builtin_bytes(*args, **kwargs)
89+
else:
90+
result = args[0].bytes(*args[1:], **kwargs)
8491

85-
result = builtin_bytes(*args, **kwargs)
8692
if isinstance(args[0], TEXT_TYPES) and is_pyobject_tainted(args[0]):
8793
try:
8894
taint_pyobject_with_ranges(result, tuple(get_ranges(args[0])))
@@ -92,13 +98,16 @@ def bytes_aspect(orig_function, flag_added_args, *args, **kwargs):
9298

9399

94100
def bytearray_aspect(orig_function, flag_added_args, *args, **kwargs):
95-
# type: (Callable, int, Any, Any) -> bytearray
96-
if orig_function != builtin_bytearray:
97-
if flag_added_args > 0:
98-
args = args[flag_added_args:]
99-
return orig_function(*args, **kwargs)
101+
# type: (Optional[Callable], int, Any, Any) -> bytearray
102+
if orig_function:
103+
if orig_function != builtin_bytearray:
104+
if flag_added_args > 0:
105+
args = args[flag_added_args:]
106+
return orig_function(*args, **kwargs)
107+
result = builtin_bytearray(*args, **kwargs)
108+
else:
109+
result = args[0].bytearray(*args[1:], **kwargs)
100110

101-
result = builtin_bytearray(*args, **kwargs)
102111
if isinstance(args[0], TEXT_TYPES) and is_pyobject_tainted(args[0]):
103112
try:
104113
taint_pyobject_with_ranges(result, tuple(get_ranges(args[0])))
@@ -108,7 +117,9 @@ def bytearray_aspect(orig_function, flag_added_args, *args, **kwargs):
108117

109118

110119
def join_aspect(orig_function, flag_added_args, *args, **kwargs):
111-
# type: (Callable, int, Any, Any) -> Any
120+
# type: (Optional[Callable], int, Any, Any) -> Any
121+
if not orig_function:
122+
orig_function = args[0].join
112123
if not isinstance(orig_function, BuiltinFunctionType):
113124
if flag_added_args > 0:
114125
args = args[flag_added_args:]
@@ -158,16 +169,22 @@ def slice_aspect(candidate_text, start, stop, step) -> Any:
158169

159170

160171
def bytearray_extend_aspect(orig_function, flag_added_args, *args, **kwargs):
161-
# type: (Callable, int, Any, Any) -> Any
162-
if not isinstance(orig_function, BuiltinFunctionType):
172+
# type: (Optional[Callable], int, Any, Any) -> Any
173+
if orig_function and not isinstance(orig_function, BuiltinFunctionType):
163174
if flag_added_args > 0:
164175
args = args[flag_added_args:]
165176
return orig_function(*args, **kwargs)
166177

178+
if len(args) < 2:
179+
# If we're not receiving at least 2 arguments, means the call was
180+
# ``x.extend()`` and not ``x.extend(y)``
181+
# so either not the extend we're looking for, or no changes in taint ranges.
182+
return args[0].extend(*args[1:], **kwargs)
183+
167184
op1 = args[0]
168185
op2 = args[1]
169186
if not isinstance(op1, bytearray) or not isinstance(op2, (bytearray, bytes)):
170-
return op1.extend(op2)
187+
return op1.extend(*args[1:], **kwargs)
171188
try:
172189
return _extend_aspect(op1, op2)
173190
except Exception as e:
@@ -217,7 +234,9 @@ def build_string_aspect(*args): # type: (List[Any]) -> str
217234

218235

219236
def ljust_aspect(orig_function, flag_added_args, *args, **kwargs):
220-
# type: (Callable, int, Any, Any) -> Union[str, bytes, bytearray]
237+
# type: (Optional[Callable], int, Any, Any) -> Union[str, bytes, bytearray]
238+
if not orig_function:
239+
orig_function = args[0].ljust
221240
if not isinstance(orig_function, BuiltinFunctionType):
222241
if flag_added_args > 0:
223242
args = args[flag_added_args:]
@@ -252,8 +271,8 @@ def ljust_aspect(orig_function, flag_added_args, *args, **kwargs):
252271

253272

254273
def zfill_aspect(orig_function, flag_added_args, *args, **kwargs):
255-
# type: (Callable, int, Any, Any) -> Any
256-
if not isinstance(orig_function, BuiltinFunctionType):
274+
# type: (Optional[Callable], int, Any, Any) -> Any
275+
if orig_function and not isinstance(orig_function, BuiltinFunctionType):
257276
if flag_added_args > 0:
258277
args = args[flag_added_args:]
259278
return orig_function(*args, **kwargs)
@@ -295,11 +314,14 @@ def zfill_aspect(orig_function, flag_added_args, *args, **kwargs):
295314

296315

297316
def format_aspect(
298-
orig_function, # type: Callable
317+
orig_function, # type: Optional[Callable]
299318
flag_added_args, # type: int
300319
*args, # type: Any
301320
**kwargs, # type: Dict[str, Any]
302321
): # type: (...) -> str
322+
if not orig_function:
323+
orig_function = args[0].format
324+
303325
if not isinstance(orig_function, BuiltinFunctionType):
304326
if flag_added_args > 0:
305327
args = args[flag_added_args:]
@@ -347,8 +369,10 @@ def format_aspect(
347369
return result
348370

349371

350-
def format_map_aspect(orig_function, flag_added_args, *args, **kwargs): # type: (Callable, int, Any, Any) -> str
351-
if not isinstance(orig_function, BuiltinFunctionType):
372+
def format_map_aspect(
373+
orig_function, flag_added_args, *args, **kwargs
374+
): # type: (Optional[Callable], int, Any, Any) -> str
375+
if orig_function and not isinstance(orig_function, BuiltinFunctionType):
352376
if flag_added_args > 0:
353377
args = args[flag_added_args:]
354378
return orig_function(*args, **kwargs)
@@ -494,12 +518,14 @@ def incremental_translation(self, incr_coder, funcode, empty):
494518

495519

496520
def decode_aspect(orig_function, flag_added_args, *args, **kwargs):
497-
if flag_added_args > 0:
498-
self = args[0]
499-
args = args[flag_added_args:]
500-
else:
521+
if orig_function and not flag_added_args:
522+
# This patch is unexpected, so we fallback
523+
# to executing the original function
501524
return orig_function(*args, **kwargs)
502525

526+
self = args[0]
527+
args = args[(flag_added_args or 1) :]
528+
# Assume we call decode method of the first argument
503529
result = self.decode(*args, **kwargs)
504530

505531
if not is_pyobject_tainted(self) or not isinstance(self, bytes):
@@ -515,12 +541,14 @@ def decode_aspect(orig_function, flag_added_args, *args, **kwargs):
515541

516542

517543
def encode_aspect(orig_function, flag_added_args, *args, **kwargs):
518-
if flag_added_args > 0:
519-
self = args[0]
520-
args = args[flag_added_args:]
521-
else:
544+
if orig_function and not flag_added_args:
545+
# This patch is unexpected, so we fallback
546+
# to executing the original function
522547
return orig_function(*args, **kwargs)
523548

549+
self = args[0]
550+
args = args[(flag_added_args or 1) :]
551+
# Assume we call encode method of the first argument
524552
result = self.encode(*args, **kwargs)
525553

526554
if not is_pyobject_tainted(self) or not isinstance(self, str):
@@ -535,8 +563,10 @@ def encode_aspect(orig_function, flag_added_args, *args, **kwargs):
535563
return result
536564

537565

538-
def upper_aspect(orig_function, flag_added_args, *args, **kwargs): # type: (Callable, int, Any, Any) -> TEXT_TYPE
539-
if not isinstance(orig_function, BuiltinFunctionType):
566+
def upper_aspect(
567+
orig_function, flag_added_args, *args, **kwargs
568+
): # type: (Optional[Callable], int, Any, Any) -> TEXT_TYPE
569+
if orig_function and not isinstance(orig_function, BuiltinFunctionType):
540570
if flag_added_args > 0:
541571
args = args[flag_added_args:]
542572
return orig_function(*args, **kwargs)
@@ -553,17 +583,17 @@ def upper_aspect(orig_function, flag_added_args, *args, **kwargs): # type: (Cal
553583
return candidate_text.upper(*args, **kwargs)
554584

555585

556-
def lower_aspect(orig_function, flag_added_args, *args, **kwargs): # type: (Callable, int, Any, Any) -> TEXT_TYPE
557-
if not isinstance(orig_function, BuiltinFunctionType):
586+
def lower_aspect(
587+
orig_function, flag_added_args, *args, **kwargs
588+
): # type: (Optional[Callable], int, Any, Any) -> TEXT_TYPE
589+
if orig_function and not isinstance(orig_function, BuiltinFunctionType):
558590
if flag_added_args > 0:
559591
args = args[flag_added_args:]
560592
return orig_function(*args, **kwargs)
561593

562594
candidate_text = args[0]
563595
args = args[flag_added_args:]
564596
if not isinstance(candidate_text, TEXT_TYPES):
565-
if flag_added_args > 0:
566-
args = args[flag_added_args:]
567597
return candidate_text.lower(*args, **kwargs)
568598

569599
try:
@@ -573,8 +603,10 @@ def lower_aspect(orig_function, flag_added_args, *args, **kwargs): # type: (Cal
573603
return candidate_text.lower(*args, **kwargs)
574604

575605

576-
def swapcase_aspect(orig_function, flag_added_args, *args, **kwargs): # type: (Callable, int, Any, Any) -> TEXT_TYPE
577-
if not isinstance(orig_function, BuiltinFunctionType):
606+
def swapcase_aspect(
607+
orig_function, flag_added_args, *args, **kwargs
608+
): # type: (Optional[Callable], int, Any, Any) -> TEXT_TYPE
609+
if orig_function and not isinstance(orig_function, BuiltinFunctionType):
578610
if flag_added_args > 0:
579611
args = args[flag_added_args:]
580612
return orig_function(*args, **kwargs)
@@ -590,8 +622,10 @@ def swapcase_aspect(orig_function, flag_added_args, *args, **kwargs): # type: (
590622
return candidate_text.swapcase(*args, **kwargs)
591623

592624

593-
def title_aspect(orig_function, flag_added_args, *args, **kwargs): # type: (Callable, int, Any, Any) -> TEXT_TYPE
594-
if not isinstance(orig_function, BuiltinFunctionType):
625+
def title_aspect(
626+
orig_function, flag_added_args, *args, **kwargs
627+
): # type: (Optional[Callable], int, Any, Any) -> TEXT_TYPE
628+
if orig_function and not isinstance(orig_function, BuiltinFunctionType):
595629
if flag_added_args > 0:
596630
args = args[flag_added_args:]
597631
return orig_function(*args, **kwargs)
@@ -607,8 +641,10 @@ def title_aspect(orig_function, flag_added_args, *args, **kwargs): # type: (Cal
607641
return candidate_text.title(*args, **kwargs)
608642

609643

610-
def capitalize_aspect(orig_function, flag_added_args, *args, **kwargs): # type: (Callable, int, Any, Any) -> TEXT_TYPE
611-
if not isinstance(orig_function, BuiltinFunctionType):
644+
def capitalize_aspect(
645+
orig_function, flag_added_args, *args, **kwargs
646+
): # type: (Optional[Callable], int, Any, Any) -> TEXT_TYPE
647+
if orig_function and not isinstance(orig_function, BuiltinFunctionType):
612648
if flag_added_args > 0:
613649
args = args[flag_added_args:]
614650
return orig_function(*args, **kwargs)
@@ -625,16 +661,21 @@ def capitalize_aspect(orig_function, flag_added_args, *args, **kwargs): # type:
625661
return candidate_text.capitalize(*args, **kwargs)
626662

627663

628-
def casefold_aspect(orig_function, flag_added_args, *args, **kwargs): # type: (Callable, int, Any, Any) -> TEXT_TYPE
629-
if not isinstance(orig_function, BuiltinFunctionType):
630-
if flag_added_args > 0:
631-
args = args[flag_added_args:]
632-
return orig_function(*args, **kwargs)
664+
def casefold_aspect(
665+
orig_function, flag_added_args, *args, **kwargs
666+
): # type: (Optional[Callable], int, Any, Any) -> TEXT_TYPE
667+
if orig_function:
668+
if not isinstance(orig_function, BuiltinFunctionType):
669+
if flag_added_args > 0:
670+
args = args[flag_added_args:]
671+
return orig_function(*args, **kwargs)
672+
else:
673+
orig_function = getattr(args[0], "casefold", None)
633674

634-
if orig_function.__qualname__ not in ("str.casefold", "bytes.casefold", "bytearray.casefold"):
675+
if orig_function and orig_function.__qualname__ not in ("str.casefold", "bytes.casefold", "bytearray.casefold"):
635676
if flag_added_args > 0:
636677
args = args[flag_added_args:]
637-
return orig_function(args, **kwargs)
678+
return orig_function(*args, **kwargs)
638679

639680
candidate_text = args[0]
640681
args = args[flag_added_args:]
@@ -649,8 +690,10 @@ def casefold_aspect(orig_function, flag_added_args, *args, **kwargs): # type: (
649690
return candidate_text.casefold(*args, **kwargs) # type: ignore[union-attr]
650691

651692

652-
def translate_aspect(orig_function, flag_added_args, *args, **kwargs): # type: (Callable, int, Any, Any) -> TEXT_TYPE
653-
if not isinstance(orig_function, BuiltinFunctionType):
693+
def translate_aspect(
694+
orig_function, flag_added_args, *args, **kwargs
695+
): # type: (Optional[Callable], int, Any, Any) -> TEXT_TYPE
696+
if orig_function and not isinstance(orig_function, BuiltinFunctionType):
654697
if flag_added_args > 0:
655698
args = args[flag_added_args:]
656699
return orig_function(*args, **kwargs)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
Vulnerability Management for Code-level (IAST): This fix resolves an issue where, at AST patching to replace code with IAST aspects, passing the original function/method as an extra parameter for accurate patching unintentionally triggers side effects in methods obtained from an expression (like ``decode`` in ``file.read(n).decode()``), resulting in unexpected multiple calls to the expression (``file.read(n)`` in the example).

0 commit comments

Comments
 (0)