Skip to content

Commit 3e16e51

Browse files
fix(iast): guard taint queries without a context (#18996)
## Description This fixes a recurring crash where IAST dereferences a Python type object that has already been freed by the cyclic GC. ``` Error UnixSignal: Process terminated with SEGV_MAPERR (SIGSEGV) #0 0x00006553759b6e73 PyType_IsSubtype (/usr/src/python/Objects/typeobject.c:2137) #1 0x0000000000000000 PyObject_TypeCheck (/opt/python/cp312-cp312/include/python3.12/object.h:381) #2 0x0000000000000000 is_text(_object const*) (/go/src/github.com/DataDog/apm-reliability/dd-trace-py/ddtrace/appsec/_iast/_taint_tracking/utils/string_utils.h:66) #3 0x0000000000000000 is_text(_object const*) (/go/src/github.com/DataDog/apm-reliability/dd-trace-py/ddtrace/appsec/_iast/_taint_tracking/utils/string_utils.h:51) #4 0x00007f20d5851de1 PyObject_TypeCheck (/opt/python/cp312-cp312/include/python3.12/object.h:381) #5 0x00007f20d5851de1 is_text(_object const*) (/go/src/github.com/DataDog/apm-reliability/dd-trace-py/ddtrace/appsec/_iast/_taint_tracking/utils/string_utils.h:66) #6 0x00007f20d5851de1 is_text(_object const*) (/go/src/github.com/DataDog/apm-reliability/dd-trace-py/ddtrace/appsec/_iast/_taint_tracking/utils/string_utils.h:51) #7 0x00007f20d5851de1 TaintEngineContext::get_tainted_object_map(_object*) (/go/src/github.com/DataDog/apm-reliability/dd-trace-py/ddtrace/appsec/_iast/_taint_tracking/context/taint_engine_context.cpp:141) #8 0x0000000000000000 std::__shared_count<(__gnu_cxx::_Lock_policy)2>::~__shared_count() (/opt/rh/devtoolset-10/root/usr/include/c++/10/bits/shared_ptr_base.h:732) #9 0x0000000000000000 std::__shared_ptr<absl::lts_20250127::node_hash_map<unsigned long, std::pair<long, std::shared_ptr<TaintedObject> >, absl::lts_20250127::hash_internal::Hash<unsigned long>, std::equal_to<unsigned long>, std::allocator<std::pair<unsigned long const, std::pair<long, std::shared_ptr<TaintedObject> > > > >, (__gnu_cxx::_Lock_policy)2>::~__shared_ptr() (/opt/rh/devtoolset-10/root/usr/include/c++/10/bits/shared_ptr_base.h:1183) #10 0x0000000000000000 std::shared_ptr<absl::lts_20250127::node_hash_map<unsigned long, std::pair<long, std::shared_ptr<TaintedObject> >, absl::lts_20250127::hash_internal::Hash<unsigned long>, std::equal_to<unsigned long>, std::allocator<std::pair<unsigned long const, std::pair<long, std::shared_ptr<TaintedObject> > > > > >::~shared_ptr() (/opt/rh/devtoolset-10/root/usr/include/c++/10/bits/shared_ptr.h:121) #11 0x0000000000000000 operator() (/go/src/github.com/DataDog/apm-reliability/dd-trace-py/ddtrace/appsec/_iast/_taint_tracking/context/taint_engine_context.cpp:357) #12 0x0000000000000000 call_impl<bool, pyexport_taint_engine_context(pybind11::module&)::<lambda(pybind11::object)>&, 0, pybind11::detail::void_type> (/go/src/github.com/DataDog/apm-reliability/dd-trace-py/ddtrace/appsec/_iast/_taint_tracking/_vendor/pybind11/include/pybind11/cast.h:2137) #13 0x0000000000000000 call<bool, pybind11::detail::void_type, pyexport_taint_engine_context(pybind11::module&)::<lambda(pybind11::object)>&> (/go/src/github.com/DataDog/apm-reliability/dd-trace-py/ddtrace/appsec/_iast/_taint_tracking/_vendor/pybind11/include/pybind11/cast.h:2105) #14 0x0000000000000000 operator() (/go/src/github.com/DataDog/apm-reliability/dd-trace-py/ddtrace/appsec/_iast/_taint_tracking/_vendor/pybind11/include/pybind11/pybind11.h:430) #15 0x00007f20d5852a70 std::__shared_count<(__gnu_cxx::_Lock_policy)2>::~__shared_count() (/opt/rh/devtoolset-10/root/usr/include/c++/10/bits/shared_ptr_base.h:732) #16 0x00007f20d5852a70 std::__shared_ptr<absl::lts_20250127::node_hash_map<unsigned long, std::pair<long, std::shared_ptr<TaintedObject> >, absl::lts_20250127::hash_internal::Hash<unsigned long>, std::equal_to<unsigned long>, std::allocator<std::pair<unsigned long const, std::pair<long, std::shared_ptr<TaintedObject> > > > >, (__gnu_cxx::_Lock_policy)2>::~__shared_ptr() (/opt/rh/devtoolset-10/root/usr/include/c++/10/bits/shared_ptr_base.h:1183) #17 0x00007f20d5852a70 std::shared_ptr<absl::lts_20250127::node_hash_map<unsigned long, std::pair<long, std::shared_ptr<TaintedObject> >, absl::lts_20250127::hash_internal::Hash<unsigned long>, std::equal_to<unsigned long>, std::allocator<std::pair<unsigned long const, std::pair<long, std::shared_ptr<TaintedObject> > > > > >::~shared_ptr() (/opt/rh/devtoolset-10/root/usr/include/c++/10/bits/shared_ptr.h:121) #18 0x00007f20d5852a70 operator() (/go/src/github.com/DataDog/apm-reliability/dd-trace-py/ddtrace/appsec/_iast/_taint_tracking/context/taint_engine_context.cpp:357) #19 0x00007f20d5852a70 call_impl<bool, pyexport_taint_engine_context(pybind11::module&)::<lambda(pybind11::object)>&, 0, pybind11::detail::void_type> (/go/src/github.com/DataDog/apm-reliability/dd-trace-py/ddtrace/appsec/_iast/_taint_tracking/_vendor/pybind11/include/pybind11/cast.h:2137) #20 0x00007f20d5852a70 call<bool, pybind11::detail::void_type, pyexport_taint_engine_context(pybind11::module&)::<lambda(pybind11::object)>&> (/go/src/github.com/DataDog/apm-reliability/dd-trace-py/ddtrace/appsec/_iast/_taint_tracking/_vendor/pybind11/include/pybind11/cast.h:2105) #21 0x00007f20d5852a70 operator() (/go/src/github.com/DataDog/apm-reliability/dd-trace-py/ddtrace/appsec/_iast/_taint_tracking/_vendor/pybind11/include/pybind11/pybind11.h:430) #22 0x00007f20d5852a70 pybind11::cpp_function::initialize<pyexport_taint_engine_context(pybind11::module_&)::{lambda(pybind11::object)#5}, bool, pybind11::object, pybind11::name, pybind11::scope, pybind11::sibling>(pyexport_taint_engine_context(pybind11::module_&)::{lambda(pybind11::object&&)#5}, bool (*)(pybind11::object), pybind11::name const, pybind11::scope&, pybind11::sibling)::{lambda(pybind11::detail::function_call&)#3}::_FUN(pybind11::detail::function_call&) [clone .cold] (/go/src/github.com/DataDog/apm-reliability/dd-trace-py/ddtrace/appsec/_iast/_taint_tracking/_vendor/pybind11/include/pybind11/pybind11.h:400) #23 0x00007f20d5809d00 pybind11::cpp_function::dispatcher(_object*, _object*, _object*) (/go/src/github.com/DataDog/apm-reliability/dd-trace-py/ddtrace/appsec/_iast/_taint_tracking/_vendor/pybind11/include/pybind11/pybind11.h:1063) #24 0x00006553759af7cf cfunction_call (/usr/src/python/Objects/methodobject.c:551) #25 0x00006553759a05de _PyObject_MakeTpCall (/usr/src/python/Objects/call.c:245) #26 0x00006553759c7357 _PyEval_EvalFrameDefault (/usr/src/python/Python/bytecodes.c:2719) #27 0x0000655375a8fa91 _PyObject_VectorcallTstate (/usr/src/python/./Include/internal/pycore_call.h:93) #28 0x0000655375984894 partial_vectorcall (/usr/src/python/./Modules/_functoolsmodule.c:269) #29 0x00006553759a0ad4 object_vacall (/usr/src/python/Objects/call.c:850) #30 0x00006553759fdd3e PyObject_CallFunctionObjArgs (/usr/src/python/Objects/call.c:961) #31 0x00007f20ded79d0a WraptBoundFunctionWrapper_call (/project/src/wrapt/_wrappers.c:3024) #32 0x00006553759a05de _PyObject_MakeTpCall (/usr/src/python/Objects/call.c:245) #33 0x00006553759c7357 _PyEval_EvalFrameDefault (/usr/src/python/Python/bytecodes.c:2719) #34 0x00006553759a3a86 gen_send_ex2 (/usr/src/python/Objects/genobject.c:238) #35 0x00007f20e038fdc7 task_step_impl (/usr/src/python/Modules/_asynciomodule.c:2869) #36 0x00007f20e03905a2 task_step (/usr/src/python/Modules/_asynciomodule.c:3188) #37 0x0000000000000000 __Pyx_PyObject_Call (/project/uvloop/loop.c:191431) #38 0x00007f20c37988eb __Pyx_PyObject_Call (/project/uvloop/loop.c:191431) #39 0x00007f20c37988eb __Pyx_PyObject_FastCallDict (/project/uvloop/loop.c:191552) #40 0x00007f20c3874a69 __pyx_f_6uvloop_4loop_6Handle__run (/project/uvloop/loop.c:66873) #41 0x00007f20c387896b __pyx_f_6uvloop_4loop_4Loop__on_idle (/project/uvloop/loop.c:17975) #42 0x00007f20c3872e52 __pyx_f_6uvloop_4loop_6Handle__run (/project/uvloop/loop.c:66927) #43 0x00007f20c3874c88 __pyx_f_6uvloop_4loop_cb_idle_callback (/project/uvloop/loop.c:87335) #44 0x0000000000000000 uv__queue_empty (/project/build/libuv-x86_64/src/queue.h:33) #45 0x00007f20c3890311 uv__queue_empty (/project/build/libuv-x86_64/src/queue.h:33) #46 0x00007f20c3890311 uv__run_idle (/project/build/libuv-x86_64/src/unix/loop-watcher.c:68) #47 0x00007f20c388d647 uv_run (/project/build/libuv-x86_64/src/unix/core.c:440) #48 0x00007f20c37aedb5 __pyx_f_6uvloop_4loop_4Loop__Loop__run (/project/uvloop/loop.c:18471) #49 0x00007f20c3816e50 __pyx_f_6uvloop_4loop_4Loop__run (/project/uvloop/loop.c:18876) #50 0x0000000000000000 __pyx_pf_6uvloop_4loop_4Loop_24run_forever (/project/uvloop/loop.c:31528) #51 0x00007f20c3827cf0 __pyx_pf_6uvloop_4loop_4Loop_24run_forever (/project/uvloop/loop.c:31528) #52 0x00007f20c3827cf0 __pyx_pw_6uvloop_4loop_4Loop_25run_forever (/project/uvloop/loop.c:31331) #53 0x00006553759a147c PyObject_VectorcallMethod (/usr/src/python/Objects/call.c:887) #54 0x0000000000000000 Py_DECREF (/opt/_internal/cpython-3.12.11/include/python3.12/object.h:700) #55 0x0000000000000000 Py_XDECREF (/opt/_internal/cpython-3.12.11/include/python3.12/object.h:798) #56 0x00007f20c382bd60 Py_DECREF (/opt/_internal/cpython-3.12.11/include/python3.12/object.h:700) #57 0x00007f20c382bd60 Py_XDECREF (/opt/_internal/cpython-3.12.11/include/python3.12/object.h:798) #58 0x00007f20c382bd60 __pyx_pf_6uvloop_4loop_4Loop_44run_until_complete (/project/uvloop/loop.c:33769) #59 0x0000000000000000 Py_XDECREF (/opt/_internal/cpython-3.12.11/include/python3.12/object.h:797) #60 0x00007f20c382d591 Py_XDECREF (/opt/_internal/cpython-3.12.11/include/python3.12/object.h:797) #61 0x00007f20c382d591 __pyx_pw_6uvloop_4loop_4Loop_45run_until_complete (/project/uvloop/loop.c:33322) #62 0x00006553759a08f8 PyObject_Vectorcall (/usr/src/python/Objects/call.c:325) #63 0x00006553759c7357 _PyEval_EvalFrameDefault (/usr/src/python/Python/bytecodes.c:2719) #64 0x00006553759a3a86 gen_send_ex2 (/usr/src/python/Objects/genobject.c:238) #65 0x00007f20e038fdc7 task_step_impl (/usr/src/python/Modules/_asynciomodule.c:2869) #66 0x00007f20e03905a2 task_step (/usr/src/python/Modules/_asynciomodule.c:3188) #67 0x00006553759a05de _PyObject_MakeTpCall (/usr/src/python/Objects/call.c:245) #68 0x000065537592377a context_run (/usr/src/python/Python/context.c:668) #69 0x0000655375a12beb cfunction_vectorcall_FASTCALL_KEYWORDS (/usr/src/python/Objects/methodobject.c:439) #70 0x0000655375919275 _PyEval_EvalFrameDefault (/usr/src/python/Python/bytecodes.c:3227) #71 0x0000655375a4a679 PyEval_EvalCode (/usr/src/python/Python/ceval.c:579) #72 0x0000655375a682bc run_eval_code_obj (/usr/src/python/Python/pythonrun.c:1723) #73 0x0000655375a68234 run_mod (/usr/src/python/Python/pythonrun.c:1744) #74 0x0000655375a67df1 pyrun_file (/usr/src/python/Python/pythonrun.c:1643) #75 0x0000655375a67c37 _PyRun_SimpleFileObject (/usr/src/python/Python/pythonrun.c:433) #76 0x0000655375a67a57 _PyRun_AnyFileObject (/usr/src/python/Python/pythonrun.c:78) #77 0x0000655375a71fb0 Py_RunMain (/usr/src/python/Modules/main.c:713) #78 0x0000655375a71b3d Py_BytesMain (/usr/src/python/Modules/main.c:768) #79 0x00007f20e1519d90 __libc_start_call_main (sysdeps/nptl/libc_start_call_main.h:58) #80 0x0000000000000000 call_init (csu/libc-start.c:128) #81 0x00007f20e1519e40 call_init (csu/libc-start.c:128) #82 0x00007f20e1519e40 __libc_start_main_alias_2 (csu/libc-start.c:379) #83 0x00006553759ea075 _start ``` Root cause (as identified by LLM): - `is_pyobject_tainted` is forwarded to the native `is_in_taint_map(obj, context_id)` - When no IAST request context is active, `context_id` is `None`, so the native binding takes the multi-slot fallback `get_tainted_object_map(obj)`, whose very first step is `is_text(obj)` - `is_text` validates the object pointer (non-null, aligned, non-null `ob_type`) but cannot detect a freed type - `PyUnicode_Check` → `PyObject_TypeCheck` → `PyType_IsSubtype` then walks the dangling type and faults. This happens when the query is invoked from an object finalizer / async-generator teardown run by the GC (`uvloop`'s `run_until_complete` is in the reported stack), i.e. mid-lifecycle rather than at interpreter exit, so the existing `shutting_down` guard (added in #18534, only set from the `atexit` handler) does not cover it. No other code seems to be affected by this specific issue. **Note** the bug affects at least versions 3.19 and 4.8, so I will backport all the way to 4.8 (and we can do 3.19 if AppSec think it's worth doing). ### Changes - `is_pyobject_tainted` now early-returns `False` when `is_iast_request_enabled()` is false, matching `get_tainted_ranges`. - The `get_ranges` public wrapper (`_taint_tracking/__init__.py`): short-circuit to `[]` when the resolved `context_id` is `None`, so no-request callers can't reach the same `is_text`-on-a-freed-type native path. - Added regression tests: no-context query does not consult native; a `__del__`/GC finalizer querying taint with no active context is safe and returns `False`; raw `get_ranges` returns empty without a context. <!-- - Reworked `test_in_taint_map_scans_container_across_active_maps` → `test_is_pyobject_tainted_is_scoped_to_active_slot`: the old test relied on the now-removed no-context multi-slot scan (the crash vector). It now asserts per-slot scoped visibility plus no-context → `False`. The genuine container-scan capability remains covered by the native `GetCurrentContextMap_{List,Tuple,Dict}Scan` C++ tests, which run with an active context. --> Co-authored-by: Alberto Vara <[email protected]>
1 parent dd06019 commit 3e16e51

5 files changed

Lines changed: 107 additions & 15 deletions

File tree

ddtrace/appsec/_iast/_taint_tracking/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ def get_ranges(string_input: Any, context_id: Optional[int] = None) -> Any:
5959

6060
_CACHE_GET_IAST_CONTEXT_ID = _get_iast_context_id
6161
context_id = _CACHE_GET_IAST_CONTEXT_ID()
62+
if context_id is None:
63+
return []
6264
return _native_get_ranges(string_input, context_id)
6365

6466

ddtrace/appsec/_iast/_taint_tracking/_taint_objects_base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Any
22

33
from ddtrace.appsec._constants import IAST
4+
from ddtrace.appsec._iast._iast_env import in_iast_env
45
from ddtrace.appsec._iast._iast_request_context_base import _get_iast_context_id
56
from ddtrace.appsec._iast._iast_request_context_base import is_iast_request_enabled
67
from ddtrace.appsec._iast._logs import iast_propagation_debug_log
@@ -77,6 +78,9 @@ def get_tainted_ranges(pyobject: Any) -> tuple:
7778

7879

7980
def is_pyobject_tainted(pyobject: Any) -> bool:
81+
if not is_iast_request_enabled() and not in_iast_env():
82+
return False
83+
8084
if not isinstance(pyobject, IAST.TAINTEABLE_TYPES):
8185
return False
8286

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
fixes:
3+
- |
4+
IAST: A crash occurring when using ``uvloop`` has been fixed.
5+

scripts/check_constant_log_message.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"ddtrace/appsec/_iast/_logs.py:41",
2929
"ddtrace/appsec/_iast/_logs.py:45",
3030
# the non constant part is an object type
31-
"ddtrace/appsec/_iast/_taint_tracking/_taint_objects_base.py:75",
31+
"ddtrace/appsec/_iast/_taint_tracking/_taint_objects_base.py:76",
3232
# _safelog wrapper function dispatches to log methods with variable message
3333
"ddtrace/internal/writer/writer.py:103",
3434
}

tests/appsec/iast/taint_tracking/test_context.py

Lines changed: 95 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import gc
2+
13
from ddtrace.appsec._iast._iast_request_context_base import IAST_CONTEXT
24
from ddtrace.appsec._iast._iast_request_context_base import _iast_finish_request
35
from ddtrace.appsec._iast._iast_request_context_base import _num_objects_tainted_in_request
46
from ddtrace.appsec._iast._taint_tracking import OriginType
7+
from ddtrace.appsec._iast._taint_tracking import Source as TaintRangeSource
58
from ddtrace.appsec._iast._taint_tracking import TaintRange
9+
from ddtrace.appsec._iast._taint_tracking import get_ranges
610
from ddtrace.appsec._iast._taint_tracking._context import clear_all_request_context_slots
711
from ddtrace.appsec._iast._taint_tracking._context import debug_num_tainted_objects
812
from ddtrace.appsec._iast._taint_tracking._context import finish_request_context
@@ -81,11 +85,10 @@ def test_taint_object_with_no_context_is_noop():
8185
assert _num_objects_tainted_in_request() == 0
8286

8387
# taint with ranges is also a no-op returning False
84-
from ddtrace.appsec._iast._taint_tracking import Source as TaintRangeSource
8588

8689
res = taint_pyobject_with_ranges(
8790
arg,
88-
[TaintRange(0, len(arg), TaintRangeSource(arg, "request_body", OriginType.PARAMETER), [])],
91+
(TaintRange(0, len(arg), TaintRangeSource(arg, "request_body", OriginType.PARAMETER), []),),
8992
)
9093
assert res is False
9194

@@ -108,23 +111,35 @@ def test_get_tainted_ranges_returns_empty_without_context():
108111
assert get_tainted_ranges("xyz") == tuple()
109112

110113

111-
def test_in_taint_map_scans_container_across_active_maps():
114+
def test_is_pyobject_tainted_is_scoped_to_active_slot():
115+
"""Taint queries resolve only within the active request slot."""
112116
clear_all_request_context_slots()
113117
_end_iast_context_and_oce()
114118

115-
# Start two independent maps
116119
ctx1 = start_request_context()
117120
ctx2 = start_request_context()
118121
assert ctx1 is not None and ctx2 is not None and ctx1 != ctx2
119122

120-
target = "TAINT_ME"
121-
# Taint the string into ctx1 explicitly without setting ContextVar
122-
target_tainted1 = _taint_pyobject_base(target, "p", target, OriginType.PARAMETER, contextid=ctx1)
123-
target_tainted2 = _taint_pyobject_base(target, "p", target, OriginType.PARAMETER, contextid=ctx2)
123+
# Distinct values -> distinct objects, so each lives in exactly one slot.
124+
tainted_in_1 = _taint_pyobject_base("TAINT_ONE", "p", "TAINT_ONE", OriginType.PARAMETER, contextid=ctx1)
125+
tainted_in_2 = _taint_pyobject_base("TAINT_TWO", "p", "TAINT_TWO", OriginType.PARAMETER, contextid=ctx2)
126+
127+
IAST_CONTEXT.set(ctx1)
128+
assert is_pyobject_tainted(tainted_in_1) is True
129+
assert is_pyobject_tainted(tainted_in_2) is False
130+
131+
IAST_CONTEXT.set(ctx2)
132+
assert is_pyobject_tainted(tainted_in_2) is True
133+
assert is_pyobject_tainted(tainted_in_1) is False
124134

125-
assert is_pyobject_tainted(target) is False
126-
assert is_pyobject_tainted(target_tainted1) is True
127-
assert is_pyobject_tainted(target_tainted2) is True
135+
# No active request => queries short-circuit to False (native is not consulted).
136+
IAST_CONTEXT.set(None)
137+
assert is_pyobject_tainted(tainted_in_1) is False
138+
assert is_pyobject_tainted(tainted_in_2) is False
139+
140+
finish_request_context(ctx1)
141+
finish_request_context(ctx2)
142+
IAST_CONTEXT.set(None)
128143

129144

130145
def test_num_objects_tainted_is_per_current_context():
@@ -162,8 +177,6 @@ def test_copy_ranges_to_string_without_context_is_noop():
162177
clear_all_request_context_slots()
163178
_end_iast_context_and_oce()
164179

165-
from ddtrace.appsec._iast._taint_tracking import Source as TaintRangeSource
166-
167180
src_val = "abc"
168181
ranges = [TaintRange(0, len(src_val), TaintRangeSource(src_val, "param", OriginType.PARAMETER), [])]
169182
out = copy_ranges_to_string("zzz", ranges)
@@ -203,7 +216,6 @@ def test_is_pyobject_tainted_does_not_leak_across_request_slots():
203216

204217
def test_get_ranges_public_api_does_not_leak_across_request_slots():
205218
"""The public ``get_ranges`` (used by sinks and aspects) must also be slot-scoped."""
206-
from ddtrace.appsec._iast._taint_tracking import get_ranges
207219

208220
clear_all_request_context_slots()
209221
_end_iast_context_and_oce()
@@ -229,3 +241,72 @@ def test_get_ranges_public_api_does_not_leak_across_request_slots():
229241
finish_request_context(ctx_a)
230242
finish_request_context(ctx_b)
231243
IAST_CONTEXT.set(None)
244+
245+
246+
def test_is_pyobject_tainted_without_context_does_not_consult_native():
247+
"""Regression for a teardown/GC crash (#18996).
248+
249+
With no active request context the query must return False without
250+
entering native code.
251+
"""
252+
clear_all_request_context_slots()
253+
_end_iast_context_and_oce()
254+
255+
ctx = start_request_context()
256+
assert ctx is not None
257+
tainted = _taint_pyobject_base(
258+
"http://dummy.location.com",
259+
"location",
260+
"http://dummy.location.com",
261+
OriginType.PARAMETER,
262+
contextid=ctx,
263+
)
264+
IAST_CONTEXT.set(ctx)
265+
assert is_pyobject_tainted(tainted) is True # sanity: visible within its own slot
266+
267+
# Detach the context, as happens once the request finishes but the tainted
268+
# object is still referenced by a soon-to-be-finalized structure.
269+
IAST_CONTEXT.set(None)
270+
assert is_pyobject_tainted(tainted) is False
271+
assert get_tainted_ranges(tainted) == tuple()
272+
273+
finish_request_context(ctx)
274+
IAST_CONTEXT.set(None)
275+
276+
277+
def test_taint_query_from_finalizer_without_context_is_safe():
278+
clear_all_request_context_slots()
279+
_end_iast_context_and_oce()
280+
IAST_CONTEXT.set(None)
281+
282+
results = []
283+
284+
class _QueriesTaintOnDel:
285+
def __init__(self, value):
286+
self.value = value
287+
288+
def __del__(self):
289+
results.append(is_pyobject_tainted(self.value))
290+
291+
obj = _QueriesTaintOnDel("some-tainted-looking-string")
292+
del obj
293+
gc.collect()
294+
295+
assert results == [False]
296+
297+
298+
def test_get_ranges_public_api_returns_empty_without_context():
299+
clear_all_request_context_slots()
300+
_end_iast_context_and_oce()
301+
302+
ctx = start_request_context()
303+
assert ctx is not None
304+
tainted = _taint_pyobject_base("secret", "p", "secret", OriginType.PARAMETER, contextid=ctx)
305+
306+
# The tainted object lives in a real slot, but with no active context the
307+
# public wrapper must not consult the native multi-slot resolver.
308+
IAST_CONTEXT.set(None)
309+
assert list(get_ranges(tainted)) == []
310+
311+
finish_request_context(ctx)
312+
IAST_CONTEXT.set(None)

0 commit comments

Comments
 (0)