Skip to content

Commit 1906fcd

Browse files
fix(iast): weak hash error if vulnerability is outside the context [backport 3.17] (#15038)
Backport 8940186 from #15029 to 3.17. ## Description This PR addresses an issue where using weak hashing or cipher algorithms outside of a request context (e.g., during application startup) could raise an unhandled exception. The fix ensures proper error handling when IAST operations are performed without an active request context. ### Root Cause The issue occurred in the [has_quota](cci:1://file:///home/alberto.vara/projects/dd-python/dd-trace-py/ddtrace/appsec/_iast/taint_sinks/_base.py:7:4-12:20) method of the vulnerability base class, which was not properly handling cases where there was no active request context. When IAST operations were performed outside of a request (e.g., during application startup or in unsupported frameworks), the code would attempt to access the vulnerability budget from a non-existent context, leading to an unhandled exception. ### Changes 1. Added null check for IAST context in [has_quota](cci:1://file:///home/alberto.vara/projects/dd-python/dd-trace-py/ddtrace/appsec/_iast/taint_sinks/_base.py:7:4-12:20) method to safely handle cases with no active request 2. Return `False` when no context is available, preventing further processing of vulnerabilities 3. Added test cases to verify the fix works in various scenarios 4. Updated the release notes to document the fix Co-authored-by: Alberto Vara <alberto.vara@datadoghq.com>
1 parent 43f65b0 commit 1906fcd

File tree

10 files changed

+190
-119
lines changed

10 files changed

+190
-119
lines changed

ddtrace/appsec/_iast/_span_metrics.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ def _set_span_tag_iast_executed_sink(span):
2929

3030

3131
def get_iast_span_metrics() -> Dict:
32-
env = _get_iast_env()
33-
return env.iast_span_metrics if env else dict()
32+
if env := _get_iast_env():
33+
return env.iast_span_metrics
34+
return dict()
3435

3536

3637
def reset_iast_span_metrics() -> None:

ddtrace/appsec/_iast/taint_sinks/_base.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@ class VulnerabilityBase:
6565

6666
@staticmethod
6767
def has_quota():
68-
context = _get_iast_env()
69-
return context.vulnerability_budget < asm_config._iast_max_vulnerabilities_per_requests
68+
if context := _get_iast_env():
69+
return context.vulnerability_budget < asm_config._iast_max_vulnerabilities_per_requests
70+
return False
7071

7172
@classmethod
7273
@taint_sink_deduplication

ddtrace/appsec/_iast/taint_sinks/ast_taint.py

Lines changed: 44 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from ddtrace.appsec._constants import IAST_SPAN_TAGS
55
from ddtrace.appsec._iast._iast_request_context_base import is_iast_request_enabled
6+
from ddtrace.appsec._iast._logs import iast_error
67
from ddtrace.appsec._iast._metrics import _set_metric_iast_executed_sink
78
from ddtrace.appsec._iast._span_metrics import increment_iast_span_metric
89
from ddtrace.appsec._iast.constants import DEFAULT_COMMAND_INJECTION_FUNCTIONS
@@ -22,54 +23,51 @@ def ast_function(
2223
*args: Any,
2324
**kwargs: Any,
2425
) -> Any:
25-
instance = getattr(func, "__self__", None)
26-
func_name = getattr(func, "__name__", None)
27-
cls_name = ""
28-
if instance is not None and func_name:
29-
try:
30-
cls_name = instance.__class__.__name__
31-
except AttributeError:
32-
pass
26+
try:
27+
instance = getattr(func, "__self__", None)
28+
func_name = getattr(func, "__name__", None)
29+
cls_name = ""
30+
if instance is not None and func_name:
31+
try:
32+
cls_name = instance.__class__.__name__
33+
except AttributeError:
34+
pass
3335

34-
if flag_added_args > 0:
35-
args = args[flag_added_args:]
36+
if flag_added_args > 0:
37+
args = args[flag_added_args:]
3638

37-
# print(f"func! {func}")
38-
# if hasattr(func, "__module__"):
39-
# print(f"func_name: {func_name}, module: {func.__module__}")
40-
# print(DEFAULT_SSRF_FUNCTIONS.get(func.__module__))
41-
# print(func_name in DEFAULT_SSRF_FUNCTIONS.get(func.__module__, ""))
39+
if (
40+
instance.__class__.__module__ == "random"
41+
and cls_name == "Random"
42+
and func_name in DEFAULT_WEAK_RANDOMNESS_FUNCTIONS
43+
):
44+
if is_iast_request_enabled():
45+
if WeakRandomness.has_quota():
46+
WeakRandomness.report(evidence_value=cls_name + "." + func_name)
4247

43-
if (
44-
instance.__class__.__module__ == "random"
45-
and cls_name == "Random"
46-
and func_name in DEFAULT_WEAK_RANDOMNESS_FUNCTIONS
47-
):
48-
if is_iast_request_enabled():
49-
if WeakRandomness.has_quota():
50-
WeakRandomness.report(evidence_value=cls_name + "." + func_name)
48+
# Reports Span Metrics
49+
increment_iast_span_metric(IAST_SPAN_TAGS.TELEMETRY_EXECUTED_SINK, WeakRandomness.vulnerability_type)
50+
# Report Telemetry Metrics
51+
_set_metric_iast_executed_sink(WeakRandomness.vulnerability_type)
5152

52-
# Reports Span Metrics
53-
increment_iast_span_metric(IAST_SPAN_TAGS.TELEMETRY_EXECUTED_SINK, WeakRandomness.vulnerability_type)
54-
# Report Telemetry Metrics
55-
_set_metric_iast_executed_sink(WeakRandomness.vulnerability_type)
56-
57-
elif (
58-
hasattr(func, "__module__")
59-
and DEFAULT_PATH_TRAVERSAL_FUNCTIONS.get(func.__module__)
60-
and func_name in DEFAULT_PATH_TRAVERSAL_FUNCTIONS[func.__module__]
61-
):
62-
check_and_report_path_traversal(*args, **kwargs)
63-
elif (
64-
hasattr(func, "__module__")
65-
and DEFAULT_COMMAND_INJECTION_FUNCTIONS.get(func.__module__)
66-
and func_name in DEFAULT_COMMAND_INJECTION_FUNCTIONS[func.__module__]
67-
):
68-
_iast_report_cmdi(func_name, *args, **kwargs)
69-
elif (
70-
hasattr(func, "__module__")
71-
and DEFAULT_SSRF_FUNCTIONS.get(func.__module__)
72-
and func_name in DEFAULT_SSRF_FUNCTIONS[func.__module__]
73-
):
74-
_iast_report_ssrf(func_name, func.__module__, *args, **kwargs)
53+
elif (
54+
hasattr(func, "__module__")
55+
and DEFAULT_PATH_TRAVERSAL_FUNCTIONS.get(func.__module__)
56+
and func_name in DEFAULT_PATH_TRAVERSAL_FUNCTIONS[func.__module__]
57+
):
58+
check_and_report_path_traversal(*args, **kwargs)
59+
elif (
60+
hasattr(func, "__module__")
61+
and DEFAULT_COMMAND_INJECTION_FUNCTIONS.get(func.__module__)
62+
and func_name in DEFAULT_COMMAND_INJECTION_FUNCTIONS[func.__module__]
63+
):
64+
_iast_report_cmdi(func_name, *args, **kwargs)
65+
elif (
66+
hasattr(func, "__module__")
67+
and DEFAULT_SSRF_FUNCTIONS.get(func.__module__)
68+
and func_name in DEFAULT_SSRF_FUNCTIONS[func.__module__]
69+
):
70+
_iast_report_ssrf(func_name, func.__module__, *args, **kwargs)
71+
except Exception as e:
72+
iast_error("propagation::sink_point::Error in ast_function", e)
7573
return func(*args, **kwargs)

ddtrace/appsec/_iast/taint_sinks/weak_cipher.py

Lines changed: 41 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from ddtrace.internal.logger import get_logger
1616
from ddtrace.settings.asm import config as asm_config
1717

18+
from .._logs import iast_error
1819
from .._metrics import _set_metric_iast_executed_sink
1920
from .._metrics import _set_metric_iast_instrumented_sink
2021
from .._patch_modules import WrapFunctonsForIAST
@@ -124,52 +125,58 @@ def wrapped_aux_blowfish_function(wrapped, instance, args, kwargs):
124125

125126

126127
def wrapped_rc4_function(wrapped: Callable, instance: Any, args: Any, kwargs: Any) -> Any:
127-
if is_iast_request_enabled():
128-
if WeakCipher.has_quota():
129-
WeakCipher.report(
130-
evidence_value="RC4",
131-
)
132-
# Reports Span Metrics
133-
increment_iast_span_metric(IAST_SPAN_TAGS.TELEMETRY_EXECUTED_SINK, WeakCipher.vulnerability_type)
134-
# Report Telemetry Metrics
135-
_set_metric_iast_executed_sink(WeakCipher.vulnerability_type)
136-
137-
if hasattr(wrapped, "__func__"):
138-
return wrapped.__func__(instance, *args, **kwargs)
139-
return wrapped(*args, **kwargs)
140-
141-
142-
def wrapped_function(wrapped: Callable, instance: Any, args: Any, kwargs: Any) -> Any:
143-
if is_iast_request_enabled():
144-
if hasattr(instance, "_dd_weakcipher_algorithm"):
128+
try:
129+
if is_iast_request_enabled():
145130
if WeakCipher.has_quota():
146-
evidence = instance._dd_weakcipher_algorithm + "_" + str(instance.__class__.__name__)
147-
WeakCipher.report(evidence_value=evidence)
148-
131+
WeakCipher.report(
132+
evidence_value="RC4",
133+
)
149134
# Reports Span Metrics
150135
increment_iast_span_metric(IAST_SPAN_TAGS.TELEMETRY_EXECUTED_SINK, WeakCipher.vulnerability_type)
151136
# Report Telemetry Metrics
152137
_set_metric_iast_executed_sink(WeakCipher.vulnerability_type)
153-
138+
except Exception as e:
139+
iast_error("propagation::sink_point::Error in weak_cipher.wrapped_rc4_function", e)
154140
if hasattr(wrapped, "__func__"):
155141
return wrapped.__func__(instance, *args, **kwargs)
156142
return wrapped(*args, **kwargs)
157143

158144

159-
def wrapped_cryptography_function(wrapped: Callable, instance: Any, args: Any, kwargs: Any) -> Any:
160-
if is_iast_request_enabled():
161-
algorithm_name = instance.algorithm.name.lower()
162-
if algorithm_name in get_weak_cipher_algorithms():
163-
if WeakCipher.has_quota():
164-
WeakCipher.report(
165-
evidence_value=algorithm_name,
166-
)
145+
def wrapped_function(wrapped: Callable, instance: Any, args: Any, kwargs: Any) -> Any:
146+
try:
147+
if is_iast_request_enabled():
148+
if hasattr(instance, "_dd_weakcipher_algorithm"):
149+
if WeakCipher.has_quota():
150+
evidence = instance._dd_weakcipher_algorithm + "_" + str(instance.__class__.__name__)
151+
WeakCipher.report(evidence_value=evidence)
152+
153+
# Reports Span Metrics
154+
increment_iast_span_metric(IAST_SPAN_TAGS.TELEMETRY_EXECUTED_SINK, WeakCipher.vulnerability_type)
155+
# Report Telemetry Metrics
156+
_set_metric_iast_executed_sink(WeakCipher.vulnerability_type)
157+
except Exception as e:
158+
iast_error("propagation::sink_point::Error in weak_cipher.wrapped_function", e)
159+
if hasattr(wrapped, "__func__"):
160+
return wrapped.__func__(instance, *args, **kwargs)
161+
return wrapped(*args, **kwargs)
167162

168-
# Reports Span Metrics
169-
increment_iast_span_metric(IAST_SPAN_TAGS.TELEMETRY_EXECUTED_SINK, WeakCipher.vulnerability_type)
170-
# Report Telemetry Metrics
171-
_set_metric_iast_executed_sink(WeakCipher.vulnerability_type)
172163

164+
def wrapped_cryptography_function(wrapped: Callable, instance: Any, args: Any, kwargs: Any) -> Any:
165+
try:
166+
if is_iast_request_enabled():
167+
algorithm_name = instance.algorithm.name.lower()
168+
if algorithm_name in get_weak_cipher_algorithms():
169+
if WeakCipher.has_quota():
170+
WeakCipher.report(
171+
evidence_value=algorithm_name,
172+
)
173+
174+
# Reports Span Metrics
175+
increment_iast_span_metric(IAST_SPAN_TAGS.TELEMETRY_EXECUTED_SINK, WeakCipher.vulnerability_type)
176+
# Report Telemetry Metrics
177+
_set_metric_iast_executed_sink(WeakCipher.vulnerability_type)
178+
except Exception as e:
179+
iast_error("propagation::sink_point::Error in weak_cipher.wrapped_cryptography_function", e)
173180
if hasattr(wrapped, "__func__"):
174181
return wrapped.__func__(instance, *args, **kwargs)
175182
return wrapped(*args, **kwargs)

ddtrace/appsec/_iast/taint_sinks/weak_hash.py

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .._iast_request_context_base import get_hash_object_tracking
1212
from .._iast_request_context_base import is_iast_request_enabled
1313
from .._iast_request_context_base import set_hash_object_tracking
14+
from .._logs import iast_error
1415
from .._metrics import _set_metric_iast_executed_sink
1516
from .._metrics import _set_metric_iast_instrumented_sink
1617
from .._patch_modules import WrapFunctonsForIAST
@@ -120,26 +121,32 @@ def wrapped_init_function(wrapped: Callable, instance: Any, args: Any, kwargs: A
120121
res = wrapped.__func__(instance, *args, **kwargs)
121122
else:
122123
res = wrapped(*args, **kwargs)
123-
if is_iast_request_enabled():
124-
set_hash_object_tracking(res, kwargs.get("usedforsecurity", None) is False)
124+
try:
125+
if is_iast_request_enabled():
126+
set_hash_object_tracking(res, kwargs.get("usedforsecurity", None) is False)
127+
except Exception as e:
128+
iast_error("propagation::sink_point::Error in weak_hash.wrapped_init_function", e)
125129
return res
126130

127131

128132
def wrapped_digest_function(wrapped: Callable, instance: Any, args: Any, kwargs: Any) -> Any:
129-
if is_iast_request_enabled():
130-
if (
131-
WeakHash.has_quota()
132-
and instance.name.lower() in get_weak_hash_algorithms()
133-
and get_hash_object_tracking(instance) is False
134-
):
135-
WeakHash.report(
136-
evidence_value=instance.name,
137-
)
138-
139-
# Reports Span Metrics
140-
increment_iast_span_metric(IAST_SPAN_TAGS.TELEMETRY_EXECUTED_SINK, WeakHash.vulnerability_type)
141-
# Report Telemetry Metrics
142-
_set_metric_iast_executed_sink(WeakHash.vulnerability_type)
133+
try:
134+
if is_iast_request_enabled():
135+
if (
136+
WeakHash.has_quota()
137+
and instance.name.lower() in get_weak_hash_algorithms()
138+
and get_hash_object_tracking(instance) is False
139+
):
140+
WeakHash.report(
141+
evidence_value=instance.name,
142+
)
143+
144+
# Reports Span Metrics
145+
increment_iast_span_metric(IAST_SPAN_TAGS.TELEMETRY_EXECUTED_SINK, WeakHash.vulnerability_type)
146+
# Report Telemetry Metrics
147+
_set_metric_iast_executed_sink(WeakHash.vulnerability_type)
148+
except Exception as e:
149+
iast_error("propagation::sink_point::Error in weak_hash.wrapped_digest_function", e)
143150

144151
if hasattr(wrapped, "__func__"):
145152
return wrapped.__func__(instance, *args, **kwargs)
@@ -155,31 +162,37 @@ def wrapped_sha1_function(wrapped: Callable, instance: Any, args: Any, kwargs: A
155162

156163

157164
def wrapped_new_function(wrapped: Callable, instance: Any, args: Any, kwargs: Any) -> Any:
158-
if is_iast_request_enabled():
159-
if WeakHash.has_quota() and args[0].lower() in get_weak_hash_algorithms():
160-
WeakHash.report(
161-
evidence_value=args[0].lower(),
162-
)
163-
# Reports Span Metrics
164-
increment_iast_span_metric(IAST_SPAN_TAGS.TELEMETRY_EXECUTED_SINK, WeakHash.vulnerability_type)
165-
# Report Telemetry Metrics
166-
_set_metric_iast_executed_sink(WeakHash.vulnerability_type)
165+
try:
166+
if is_iast_request_enabled():
167+
if WeakHash.has_quota() and args[0].lower() in get_weak_hash_algorithms():
168+
WeakHash.report(
169+
evidence_value=args[0].lower(),
170+
)
171+
# Reports Span Metrics
172+
increment_iast_span_metric(IAST_SPAN_TAGS.TELEMETRY_EXECUTED_SINK, WeakHash.vulnerability_type)
173+
# Report Telemetry Metrics
174+
_set_metric_iast_executed_sink(WeakHash.vulnerability_type)
175+
except Exception as e:
176+
iast_error("propagation::sink_point::Error in weak_hash.wrapped_new_function", e)
167177

168178
if hasattr(wrapped, "__func__"):
169179
return wrapped.__func__(instance, *args, **kwargs)
170180
return wrapped(*args, **kwargs)
171181

172182

173183
def wrapped_function(wrapped: Callable, evidence: str, instance: Any, args: Any, kwargs: Any) -> Any:
174-
if is_iast_request_enabled():
175-
if WeakHash.has_quota():
176-
WeakHash.report(
177-
evidence_value=evidence,
178-
)
179-
# Reports Span Metrics
180-
increment_iast_span_metric(IAST_SPAN_TAGS.TELEMETRY_EXECUTED_SINK, WeakHash.vulnerability_type)
181-
# Report Telemetry Metrics
182-
_set_metric_iast_executed_sink(WeakHash.vulnerability_type)
184+
try:
185+
if is_iast_request_enabled():
186+
if WeakHash.has_quota():
187+
WeakHash.report(
188+
evidence_value=evidence,
189+
)
190+
# Reports Span Metrics
191+
increment_iast_span_metric(IAST_SPAN_TAGS.TELEMETRY_EXECUTED_SINK, WeakHash.vulnerability_type)
192+
# Report Telemetry Metrics
193+
_set_metric_iast_executed_sink(WeakHash.vulnerability_type)
194+
except Exception as e:
195+
iast_error("propagation::sink_point::Error in weak_hash.wrapped_function", e)
183196

184197
if hasattr(wrapped, "__func__"):
185198
return wrapped.__func__(instance, *args, **kwargs)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- |
4+
IAST: Fixed an issue where using weak hashing or cipher algorithms outside of a request context
5+
(e.g., during application startup) could raise an unhandled exception. The fix ensures proper error
6+
handling when IAST operations are performed without an active request context.

tests/appsec/iast/taint_sinks/test_vulnerability_detection.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from ddtrace.appsec._iast.sampling.vulnerability_detection import reset_request_vulnerabilities
1515
from ddtrace.appsec._iast.sampling.vulnerability_detection import should_process_vulnerability
1616
from ddtrace.appsec._iast.sampling.vulnerability_detection import update_global_vulnerability_limit
17+
from ddtrace.appsec._iast.taint_sinks._base import VulnerabilityBase
1718
from tests.appsec.iast.iast_utils import _end_iast_context_and_oce
1819
from tests.appsec.iast.iast_utils import _start_iast_context_and_oce
1920
from tests.utils import override_global_config
@@ -292,3 +293,10 @@ def test_with_modified_max_vulnerabilities_config():
292293
# Global map should be updated with all processed vulnerabilities
293294
update_global_vulnerability_limit()
294295
assert len(_get_global_limit()["GET:/config_test"]) == 3
296+
297+
298+
def test_quota_out_of_context():
299+
_end_iast_context_and_oce()
300+
env = _get_iast_env()
301+
assert env is None
302+
assert VulnerabilityBase.has_quota() is False

tests/appsec/iast/taint_sinks/test_weak_cipher.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from unittest import mock
2+
13
import pytest
24

35
from ddtrace.appsec._iast._iast_request_context import get_iast_reporter
@@ -215,3 +217,18 @@ def test_weak_cipher_secure_multiple_calls_error(iast_context_defaults):
215217
span_report = get_iast_reporter()
216218

217219
assert span_report is None
220+
221+
222+
@mock.patch("ddtrace.appsec._iast.taint_sinks.weak_hash.is_iast_request_enabled")
223+
@mock.patch("ddtrace.appsec._iast.taint_sinks.weak_hash.increment_iast_span_metric")
224+
def test_weak_cipher_out_of_context(
225+
mock_is_iast_request_enabled, mock_increment_iast_span_metric, iast_context_defaults
226+
):
227+
mock_is_iast_request_enabled.return_value = True
228+
mock_increment_iast_span_metric.side_effect = Exception(
229+
"increment_iast_span_metric should not be called in this test"
230+
)
231+
try:
232+
cryptography_algorithm("Blowfish")
233+
except Exception as e:
234+
pytest.fail(f"parametrized_weak_hash raised an exception: {e}")

0 commit comments

Comments
 (0)