Skip to content

Commit 7aee559

Browse files
emmettbutlermabdinurmajorgreysavara1986gnufede
authored
feat(iast): weak randomness vulnerability [backport #6912 to 2.0] (#6988)
This change backports #6912 to the 2.0 release branch. This change also re-enables the system-tests workflow, which had been temporarily disabled in #6974 to make that pull request mergeable. The net effect of these git gymnastics is that the diff still waiting to be merged into 2.0 is as small as possible. ## 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: Munir Abdinur <[email protected]> Co-authored-by: Tahir H. Butt <[email protected]> Co-authored-by: Alberto Vara <[email protected]> Co-authored-by: Federico Mon <[email protected]>
1 parent c4e7991 commit 7aee559

File tree

13 files changed

+677
-6
lines changed

13 files changed

+677
-6
lines changed

.github/workflows/system-tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ jobs:
6767
if: needs.needs-run.outputs.outcome == 'success'
6868
run: ./build.sh
6969

70-
#- name: Run
71-
# if: needs.needs-run.outputs.outcome == 'success'
72-
#run: ./run.sh
70+
- name: Run
71+
if: needs.needs-run.outputs.outcome == 'success'
72+
run: ./run.sh
7373

7474
- name: Run REMOTE_CONFIG_MOCKED_BACKEND_ASM_FEATURES
7575
if: needs.needs-run.outputs.outcome == 'success'

ddtrace/appsec/_iast/_ast/visitor.py

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
#!/usr/bin/env python3
2-
32
from _ast import Expr
43
from _ast import ImportFrom
54
import ast
5+
import copy
66
import sys
77
from typing import TYPE_CHECKING
88

99
from six import iteritems
1010

1111
from .._metrics import _set_metric_iast_instrumented_propagation
12+
from ..constants import DEFAULT_WEAK_RANDOMNESS_FUNCTIONS
1213

1314

1415
if TYPE_CHECKING: # pragma: no cover
@@ -20,6 +21,12 @@
2021
PY30_37 = sys.version_info >= (3, 0, 0) and sys.version_info < (3, 8, 0)
2122
PY38_PLUS = sys.version_info >= (3, 8, 0)
2223

24+
CODE_TYPE_FIRST_PARTY = "first_party"
25+
CODE_TYPE_DD = "datadog"
26+
CODE_TYPE_SITE_PACKAGES = "site_packages"
27+
CODE_TYPE_STDLIB = "stdlib"
28+
TAINT_SINK_FUNCTION_REPLACEMENT = "ddtrace_taint_sinks.ast_funcion"
29+
2330

2431
class AstVisitor(ast.NodeTransformer):
2532
def __init__(
@@ -86,6 +93,26 @@ def __init__(
8693
},
8794
"django.utils.html": {"": ("format_html", "format_html_join")},
8895
},
96+
# This is a set since all functions will be replaced by taint_sink_functions
97+
"taint_sinks": {
98+
"weak_randomness": DEFAULT_WEAK_RANDOMNESS_FUNCTIONS,
99+
"other": {
100+
"load",
101+
"run",
102+
"path",
103+
"exit",
104+
"sleep",
105+
"socket",
106+
},
107+
# These explicitly WON'T be replaced by taint_sink_function:
108+
"disabled": {
109+
"__new__",
110+
"__init__",
111+
"__dir__",
112+
"__repr__",
113+
"super",
114+
},
115+
},
89116
}
90117
self._sinkpoints_spec = {
91118
"definitions_module": "ddtrace.appsec._iast.taint_sinks",
@@ -107,6 +134,12 @@ def __init__(
107134
self._aspect_build_string = self._aspects_spec["operators"]["BUILD_STRING"]
108135
self.excluded_functions = self._aspects_spec["excluded_from_patching"].get(self.module_name, {})
109136

137+
# Sink points
138+
self._taint_sink_replace_random = self._aspects_spec["taint_sinks"]["weak_randomness"]
139+
self._taint_sink_replace_other = self._aspects_spec["taint_sinks"]["other"]
140+
self._taint_sink_replace_any = self._taint_sink_replace_random.union(self._taint_sink_replace_other)
141+
self._taint_sink_replace_disabled = self._aspects_spec["taint_sinks"]["disabled"]
142+
110143
self.dont_patch_these_functionsdefs = set()
111144
for _, v in iteritems(self.excluded_functions):
112145
if v:
@@ -117,6 +150,16 @@ def __init__(
117150
# replacements and enabled again on all the others
118151
self.replacements_disabled_for_functiondef = False
119152

153+
self.codetype = CODE_TYPE_FIRST_PARTY
154+
if "ast/tests/fixtures" in self.filename:
155+
self.codetype = CODE_TYPE_FIRST_PARTY
156+
elif "ddtrace" in self.filename and ("site-packages" in self.filename or "dist-packages" in self.filename):
157+
self.codetype = CODE_TYPE_DD
158+
elif "site-packages" in self.filename or "dist-packages" in self.filename:
159+
self.codetype = CODE_TYPE_SITE_PACKAGES
160+
elif "lib/python" in self.filename:
161+
self.codetype = CODE_TYPE_STDLIB
162+
120163
def _is_string_node(self, node): # type: (Any) -> bool
121164
if PY30_37 and isinstance(node, ast.Bytes):
122165
return True
@@ -155,6 +198,42 @@ def _is_string_format_with_literals(self, call_node):
155198
and all(map(lambda x: self._is_node_constant_or_binop(x.value), call_node.keywords))
156199
)
157200

201+
def _get_function_name(self, call_node, is_function): # type: (ast.Call, bool) -> str
202+
if is_function:
203+
return call_node.func.id # type: ignore[attr-defined]
204+
# If the call is to a method
205+
elif type(call_node.func) == ast.Name:
206+
return call_node.func.id
207+
208+
return call_node.func.attr # type: ignore[attr-defined]
209+
210+
def _should_replace_with_taint_sink(self, call_node, is_function): # type: (ast.Call, bool) -> bool
211+
function_name = self._get_function_name(call_node, is_function)
212+
213+
if function_name in self._taint_sink_replace_disabled:
214+
return False
215+
216+
return any(allowed in function_name for allowed in self._taint_sink_replace_any)
217+
218+
def _add_original_function_as_arg(self, call_node, is_function): # type: (ast.Call, bool) -> Any
219+
"""
220+
Creates the arguments for the original function
221+
"""
222+
function_name = self._get_function_name(call_node, is_function)
223+
function_name_arg = (
224+
self._name_node(call_node, function_name, ctx=ast.Load()) if is_function else copy.copy(call_node.func)
225+
)
226+
227+
# Arguments for stack info change from:
228+
# my_function(self, *args, **kwargs)
229+
# to:
230+
# _add_original_function_as_arg(function_name=my_function, self, *args, **kwargs)
231+
new_args = [
232+
function_name_arg,
233+
] + call_node.args
234+
235+
return new_args
236+
158237
def _node(self, type_, pos_from_node, **kwargs):
159238
# type: (Any, Any, Any) -> Any
160239
"""
@@ -361,6 +440,24 @@ def visit_Call(self, call_node): # type: (ast.Call) -> Any
361440
call_node.func = self._attr_node(call_node, aspect)
362441
self.ast_modified = call_modified = True
363442

443+
if self.codetype == CODE_TYPE_FIRST_PARTY:
444+
# Function replacement case
445+
if isinstance(call_node.func, ast.Name):
446+
aspect = self._should_replace_with_taint_sink(call_node, True)
447+
if aspect:
448+
call_node.args = self._add_original_function_as_arg(call_node, False)
449+
call_node.func = self._attr_node(call_node, TAINT_SINK_FUNCTION_REPLACEMENT)
450+
self.ast_modified = call_modified = True
451+
452+
# Method replacement case
453+
elif isinstance(call_node.func, ast.Attribute):
454+
aspect = self._should_replace_with_taint_sink(call_node, False)
455+
if aspect:
456+
# Create a new Name node for the replacement and set it as node.func
457+
call_node.args = self._add_original_function_as_arg(call_node, False)
458+
call_node.func = self._attr_node(call_node, TAINT_SINK_FUNCTION_REPLACEMENT)
459+
self.ast_modified = call_modified = True
460+
364461
if call_modified:
365462
_set_metric_iast_instrumented_propagation()
366463

ddtrace/appsec/_iast/constants.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
VULN_WEAK_CIPHER_TYPE = "WEAK_CIPHER"
33
VULN_SQL_INJECTION = "SQL_INJECTION"
44
VULN_PATH_TRAVERSAL = "PATH_TRAVERSAL"
5+
VULN_WEAK_RANDOMNESS = "WEAK_RANDOMNESS"
56
VULN_INSECURE_COOKIE = "INSECURE_COOKIE"
67
VULN_NO_HTTPONLY_COOKIE = "NO_HTTPONLY_COOKIE"
78
VULN_NO_SAMESITE_COOKIE = "NO_SAMESITE_COOKIE"
@@ -11,6 +12,7 @@
1112
EVIDENCE_ALGORITHM_TYPE = "ALGORITHM"
1213
EVIDENCE_SQL_INJECTION = "SQL_INJECTION"
1314
EVIDENCE_PATH_TRAVERSAL = "PATH_TRAVERSAL"
15+
EVIDENCE_WEAK_RANDOMNESS = "WEAK_RANDOMNESS"
1416
EVIDENCE_COOKIE = "COOKIE"
1517
EVIDENCE_CMDI = "COMMAND"
1618

@@ -28,3 +30,25 @@
2830
DEFAULT_WEAK_HASH_ALGORITHMS = {MD5_DEF, SHA1_DEF}
2931

3032
DEFAULT_WEAK_CIPHER_ALGORITHMS = {DES_DEF, BLOWFISH_DEF, RC2_DEF, RC4_DEF, IDEA_DEF}
33+
34+
DEFAULT_WEAK_RANDOMNESS_FUNCTIONS = {
35+
"random",
36+
"randint",
37+
"randrange",
38+
"choice",
39+
"shuffle",
40+
"betavariate",
41+
"gammavariate",
42+
"expovariate",
43+
"choices",
44+
"gauss",
45+
"uniform",
46+
"lognormvariate",
47+
"normalvariate",
48+
"paretovariate",
49+
"sample",
50+
"triangular",
51+
"vonmisesvariate",
52+
"weibullvariate",
53+
"randbytes",
54+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
from ddtrace.appsec._iast.taint_sinks.path_traversal import open_path_traversal
1+
from .ast_taint import ast_funcion
2+
from .path_traversal import open_path_traversal
23

34

45
__all__ = [
56
"open_path_traversal",
7+
"ast_funcion",
68
]
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from typing import TYPE_CHECKING
2+
3+
from ..constants import DEFAULT_WEAK_RANDOMNESS_FUNCTIONS
4+
from .weak_randomness import WeakRandomness
5+
6+
7+
if TYPE_CHECKING:
8+
from typing import Any
9+
from typing import Callable
10+
11+
12+
def ast_funcion(
13+
func, # type: Callable
14+
*args, # type: Any
15+
**kwargs # type: Any
16+
): # type: (...) -> Any
17+
18+
cls = getattr(func, "__self__", None)
19+
func_name = getattr(func, "__name__", None)
20+
cls_name = ""
21+
if cls and func_name:
22+
try:
23+
cls_name = cls.__class__.__name__
24+
except AttributeError:
25+
pass
26+
27+
if cls.__class__.__module__ == "random" and cls_name == "Random" and func_name in DEFAULT_WEAK_RANDOMNESS_FUNCTIONS:
28+
# Weak, run the analyzer
29+
WeakRandomness.report(evidence_value=cls_name + "." + func_name)
30+
31+
return func(*args, **kwargs)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from .. import oce
2+
from ..constants import EVIDENCE_WEAK_RANDOMNESS
3+
from ..constants import VULN_WEAK_RANDOMNESS
4+
from ._base import VulnerabilityBase
5+
6+
7+
@oce.register
8+
class WeakRandomness(VulnerabilityBase):
9+
vulnerability_type = VULN_WEAK_RANDOMNESS
10+
evidence_type = EVIDENCE_WEAK_RANDOMNESS
11+
12+
@classmethod
13+
def report(cls, evidence_value=None, sources=None):
14+
super(WeakRandomness, cls).report(evidence_value=evidence_value)

ddtrace/appsec/iast/_ast/__init__.py

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
features:
3+
- |
4+
Vulnerability Management for Code-level (IAST): Weak randomness vulnerability detection.

0 commit comments

Comments
 (0)