From 36f686ad05e47e52ae076fc10f317aa8240a08a9 Mon Sep 17 00:00:00 2001 From: Thomas Kowalski Date: Mon, 18 May 2026 10:52:51 +0200 Subject: [PATCH 1/2] fix(iast): prevent out-of-bounds read in slice aspect with negative step --- .../_taint_tracking/aspects/aspect_slice.cpp | 44 +++++++------------ 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/ddtrace/appsec/_iast/_taint_tracking/aspects/aspect_slice.cpp b/ddtrace/appsec/_iast/_taint_tracking/aspects/aspect_slice.cpp index 244b4d4939b..e03c757cafa 100644 --- a/ddtrace/appsec/_iast/_taint_tracking/aspects/aspect_slice.cpp +++ b/ddtrace/appsec/_iast/_taint_tracking/aspects/aspect_slice.cpp @@ -65,38 +65,26 @@ build_index_range_map(PyObject* text, TaintRangeRefs& ranges, PyObject* start, P index_range_map.emplace_back(nullptr); index++; } - TaintRangeRefs index_range_map_result; - long start_int = 0; - if (start != nullptr and start != Py_None) { - start_int = PyLong_AsLong(start); - if (start_int < 0) { - start_int = length_text + start_int; - if (start_int < 0) { - start_int = 0; - } - } - } - - long stop_int = length_text; - if (stop != nullptr and stop != Py_None) { - stop_int = PyLong_AsLong(stop); - if (stop_int > length_text) { - stop_int = length_text; - } else if (stop_int < 0) { - stop_int = length_text + stop_int; - if (stop_int < 0) { - stop_int = 0; - } - } + PyObject* slice = PySlice_New( + (start != nullptr) ? start : Py_None, (stop != nullptr) ? stop : Py_None, (step != nullptr) ? step : Py_None); + if (slice == nullptr) { + return {}; } - long step_int = 1; - if (step != nullptr and step != Py_None) { - step_int = PyLong_AsLong(step); + Py_ssize_t norm_start, norm_stop, norm_step, slicelength; + if (PySlice_GetIndicesEx(slice, length_text, &norm_start, &norm_stop, &norm_step, &slicelength) < 0) { + Py_DECREF(slice); + return {}; } + Py_DECREF(slice); - for (auto i = start_int; i < stop_int; i += step_int) { - index_range_map_result.emplace_back(index_range_map[i]); + TaintRangeRefs index_range_map_result; + Py_ssize_t map_size = static_cast(index_range_map.size()); + for (Py_ssize_t i = 0; i < slicelength; i++) { + Py_ssize_t idx = norm_start + i * norm_step; + if (idx >= 0 && idx < map_size) { + index_range_map_result.emplace_back(index_range_map[idx]); + } } return index_range_map_result; From 12c71cd2b01419dfb5a8d0960fd4d53a212a3fd3 Mon Sep 17 00:00:00 2001 From: Thomas Kowalski Date: Mon, 18 May 2026 17:06:19 +0200 Subject: [PATCH 2/2] test: add regression test --- .../aspects/test_slice_aspect_fixtures.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/appsec/iast/aspects/test_slice_aspect_fixtures.py b/tests/appsec/iast/aspects/test_slice_aspect_fixtures.py index 5d537dba975..f60ebd2630e 100644 --- a/tests/appsec/iast/aspects/test_slice_aspect_fixtures.py +++ b/tests/appsec/iast/aspects/test_slice_aspect_fixtures.py @@ -288,6 +288,37 @@ def test_string_slice_negative(): assert result == expected_result +@pytest.mark.parametrize( + "input_str, start_pos, end_pos, step, expected_result", + [ + # Regression: negative step triggered OOB read in build_index_range_map + # because start_int=0 < stop_int=len satisfied i None: + """Regression test for out-of-bounds read in slice aspect with negative step.""" + result = mod.do_slice(input_str, start_pos, end_pos, step) # pylint: disable=no-member + assert result == expected_result + + tainted_input = taint_pyobject( + pyobject=input_str, + source_name="input_str", + source_value="foo", + source_origin=OriginType.PARAMETER, + ) + result = mod.do_slice(tainted_input, start_pos, end_pos, step) # pylint: disable=no-member + assert result == expected_result + tainted_ranges = get_tainted_ranges(result) + assert len(tainted_ranges) == 1 + assert tainted_ranges[0].start == 0 + assert tainted_ranges[0].length == len(expected_result) + + @pytest.mark.skip_iast_check_logs def test_propagate_ranges_with_no_context(caplog): tainted_input = taint_pyobject(