Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e5055b7

Browse files
authored
chore(iast): header source in werkzeug 3.1 (DataDog#12213)
IAST taint source Header wouldn't work correctly in `werkzeug>=3.1.0` We're tainting `EnvironHeaders.__getitem__` due to get method calls to `__getitem__` https://github.com/pallets/werkzeug/blob/2.3.8/src/werkzeug/datastructures/headers.py but now the `__getitem__` and `get` methods of `EnvironHeaders` are completely differents: https://github.com/pallets/werkzeug/blob/main/src/werkzeug/datastructures/headers.py ## Checklist - [x] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - 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)
1 parent a54a55d commit e5055b7

3 files changed

Lines changed: 75 additions & 7 deletions

File tree

ddtrace/appsec/_iast/_handlers.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,23 +82,28 @@ def _on_flask_patch(flask_version):
8282
"Headers.items",
8383
functools.partial(if_iast_taint_yield_tuple_for, (OriginType.HEADER_NAME, OriginType.HEADER)),
8484
)
85-
_set_metric_iast_instrumented_source(OriginType.HEADER_NAME)
86-
_set_metric_iast_instrumented_source(OriginType.HEADER)
8785

8886
try_wrap_function_wrapper(
8987
"werkzeug.datastructures",
90-
"ImmutableMultiDict.__getitem__",
91-
functools.partial(if_iast_taint_returned_object_for, OriginType.PARAMETER),
88+
"EnvironHeaders.__getitem__",
89+
functools.partial(if_iast_taint_returned_object_for, OriginType.HEADER),
9290
)
93-
_set_metric_iast_instrumented_source(OriginType.PARAMETER)
94-
91+
# Since werkzeug 3.1.0 get doesn't call to __getitem__
9592
try_wrap_function_wrapper(
9693
"werkzeug.datastructures",
97-
"EnvironHeaders.__getitem__",
94+
"EnvironHeaders.get",
9895
functools.partial(if_iast_taint_returned_object_for, OriginType.HEADER),
9996
)
97+
_set_metric_iast_instrumented_source(OriginType.HEADER_NAME)
10098
_set_metric_iast_instrumented_source(OriginType.HEADER)
10199

100+
try_wrap_function_wrapper(
101+
"werkzeug.datastructures",
102+
"ImmutableMultiDict.__getitem__",
103+
functools.partial(if_iast_taint_returned_object_for, OriginType.PARAMETER),
104+
)
105+
_set_metric_iast_instrumented_source(OriginType.PARAMETER)
106+
102107
if flask_version >= (2, 0, 0):
103108
# instance.query_string: raising an error on werkzeug/_internal.py "AttributeError: read only property"
104109
try_wrap_function_wrapper("werkzeug.wrappers.request", "Request.__init__", _on_request_init)

hatch.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,13 @@ flask = ["~=2.2"]
399399
python = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
400400
flask = ["~=3.0"]
401401

402+
[[envs.appsec_integrations_flask.matrix]]
403+
# werkzeug 3.1 drops support for py3.8
404+
python = ["3.11", "3.12", "3.13"]
405+
flask = ["~=3.1"]
406+
werkzeug = ["~=3.1"]
407+
408+
## ASM appsec_integrations_fastapi
402409

403410
[envs.appsec_integrations_fastapi]
404411
template = "appsec_integrations_fastapi"

tests/appsec/integrations/flask_tests/test_iast_flask.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,62 @@ def sqli_2(param_str):
166166
assert vulnerability["location"]["path"] == TEST_FILE_PATH
167167
assert vulnerability["hash"] == hash_value
168168

169+
@pytest.mark.skipif(not asm_config._iast_supported, reason="Python version not supported by IAST")
170+
def test_flask_iast_enabled_http_request_header_get(self):
171+
@self.app.route("/sqli/<string:param_str>/", methods=["GET", "POST"])
172+
def sqli_2(param_str):
173+
import sqlite3
174+
175+
from flask import request
176+
177+
from ddtrace.appsec._iast._taint_tracking.aspects import add_aspect
178+
179+
con = sqlite3.connect(":memory:")
180+
cur = con.cursor()
181+
# label test_flask_iast_enabled_http_request_header_get
182+
cur.execute(add_aspect("SELECT 1 FROM ", request.headers.get("User-Agent")))
183+
184+
return "OK", 200
185+
186+
with override_global_config(
187+
dict(
188+
_iast_enabled=True,
189+
_iast_deduplication_enabled=False,
190+
)
191+
):
192+
resp = self.client.post(
193+
"/sqli/sqlite_master/", data={"name": "test"}, headers={"User-Agent": "sqlite_master"}
194+
)
195+
assert resp.status_code == 200
196+
197+
root_span = self.pop_spans()[0]
198+
assert root_span.get_metric(IAST.ENABLED) == 1.0
199+
200+
loaded = json.loads(root_span.get_tag(IAST.JSON))
201+
assert loaded["sources"] == [
202+
{"origin": "http.request.header", "name": "User-Agent", "value": "sqlite_master"}
203+
]
204+
205+
line, hash_value = get_line_and_hash(
206+
"test_flask_iast_enabled_http_request_header_get",
207+
VULN_SQL_INJECTION,
208+
filename=TEST_FILE_PATH,
209+
)
210+
vulnerability = loaded["vulnerabilities"][0]
211+
212+
assert vulnerability["type"] == VULN_SQL_INJECTION
213+
assert vulnerability["evidence"] == {
214+
"valueParts": [
215+
{"value": "SELECT "},
216+
{"redacted": True},
217+
{"value": " FROM "},
218+
{"value": "sqlite_master", "source": 0},
219+
]
220+
}
221+
assert vulnerability["location"]["line"] == line
222+
assert vulnerability["location"]["path"] == TEST_FILE_PATH
223+
assert vulnerability["hash"] == hash_value
224+
169225
@pytest.mark.skipif(not asm_config._iast_supported, reason="Python version not supported by IAST")
170226
def test_flask_full_sqli_iast_enabled_http_request_header_name_keys(self):
171227
@self.app.route("/sqli/<string:param_str>/", methods=["GET", "POST"])

0 commit comments

Comments
 (0)