Skip to content

Commit 11b9fbd

Browse files
committed
fix(iast): weak hash error if vulnerability is outside the context (#15029)
1 parent 43f65b0 commit 11b9fbd

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)