Skip to content

Commit c610103

Browse files
fix(asm): improve cookies management for fastapi by using starlette parser (#7873)
Improve cookie management for fastapi and ASM by using starlette parser. This ensure that data sent to the waf before the request is processed by the customer code gets exactly the same cookies data as customer code. Previously, computation was done on the middleware level with Python http.cookies Also, minor improvement of asm fast api test for cookies to handle some corner case. It will also be tested in system tests. FastAPI ASM support is still a private wip feature, and this is a fix for #7220 ## Checklist - [x] Change(s) are motivated and described in the PR description. - [x] Testing strategy is described if automated tests are not included in the PR. - [x] Risk is outlined (performance impact, potential for breakage, maintainability, etc). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] [Library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) are followed. If no release note is required, add label `changelog/no-changelog`. - [x] Documentation is included (in-code, generated user docs, [public corp docs](https://github.com/DataDog/documentation/)). - [x] Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Title is accurate. - [x] No unnecessary changes are introduced. - [x] Description motivates each change. - [x] Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [x] Testing strategy adequately addresses listed risk(s). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] Release note makes sense to a user of the library. - [x] Reviewer has explicitly acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment. - [x] 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) - [x] If this PR touches code that signs or publishes builds or packages, or handles credentials of any kind, I've requested a review from `@DataDog/security-design-and-guidance`. - [x] This PR doesn't touch any of that. Co-authored-by: Alberto Vara <[email protected]>
1 parent fd12042 commit c610103

File tree

3 files changed

+26
-17
lines changed

3 files changed

+26
-17
lines changed

ddtrace/contrib/asgi/middleware.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from http import cookies
21
import sys
32
from typing import Any
43
from typing import Mapping
@@ -159,7 +158,6 @@ async def __call__(self, scope, receive, send):
159158
span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate)
160159

161160
host_header = None
162-
request_cookies = None
163161
for key, value in scope["headers"]:
164162
if key == b"host":
165163
try:
@@ -168,10 +166,7 @@ async def __call__(self, scope, receive, send):
168166
log.warning(
169167
"failed to decode host header, host from http headers will not be considered", exc_info=True
170168
)
171-
elif key == b"cookie":
172-
c = cookies.SimpleCookie(bytes_to_str(value))
173-
request_cookies = {k: v.value for k, v in c.items()}
174-
169+
break
175170
method = scope.get("method")
176171
server = scope.get("server")
177172
scheme = scope.get("scheme", "http")
@@ -212,7 +207,6 @@ async def __call__(self, scope, receive, send):
212207
query=query_string,
213208
request_headers=headers,
214209
raw_uri=url,
215-
request_cookies=request_cookies,
216210
parsed_query=parsed_query,
217211
request_body=body,
218212
peer_ip=peer_ip,

ddtrace/contrib/starlette/patch.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Optional # noqa:F401
55

66
import starlette
7+
from starlette import requests as starlette_requests
78
from starlette.middleware import Middleware
89

910
from ddtrace import config
@@ -131,8 +132,18 @@ def traced_handler(wrapped, instance, args, kwargs):
131132
request_spans,
132133
resource_paths,
133134
)
135+
request_cookies = ""
136+
for name, value in scope.get("headers"):
137+
if name == b"cookie":
138+
request_cookies = value.decode("utf-8", errors="ignore")
139+
break
134140
if request_spans:
135-
trace_utils.set_http_meta(request_spans[0], "starlette", request_path_params=scope.get("path_params"))
141+
trace_utils.set_http_meta(
142+
request_spans[0],
143+
"starlette",
144+
request_path_params=scope.get("path_params"),
145+
request_cookies=starlette_requests.cookie_parser(request_cookies),
146+
)
136147
core.dispatch("asgi.start_request", ("starlette",))
137148
if core.get_item(HTTP_REQUEST_BLOCKED):
138149
raise trace_utils.InterruptException("starlette")

tests/contrib/fastapi/test_fastapi_appsec.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22

3+
import fastapi
34
from fastapi import Request
45
from fastapi.responses import PlainTextResponse
56
from fastapi.testclient import TestClient
@@ -298,29 +299,32 @@ def test_route():
298299

299300

300301
def test_request_suspicious_request_block_match_cookies(app, client, tracer, test_spans):
301-
@app.get("/")
302-
def test_route():
303-
return PlainTextResponse("OK")
302+
@app.get("/test_cookies", response_class=PlainTextResponse)
303+
def test_route(response: fastapi.Response):
304+
response.status_code = fastapi.status.HTTP_201_CREATED
305+
return "OK Cookie"
304306

305307
# value jdfoSDGFkivRG_234 must be blocked
306308
with override_global_config(dict(_asm_enabled=True)), override_env(dict(DD_APPSEC_RULES=RULES_SRB)):
307309
_aux_appsec_prepare_tracer(tracer)
308-
resp = client.get("/", cookies={"keyname": "jdfoSDGFkivRG_234"})
309-
assert resp.status_code == 403
310+
resp = client.get("/test_cookies", cookies={"keyname": "jdfoSDGFkivRG_234 45"})
310311
assert get_response_body(resp) == constants.BLOCKED_RESPONSE_JSON
312+
assert resp.status_code == 403
311313
root_span = get_root_span(test_spans)
312314
loaded = json.loads(root_span.get_tag(APPSEC.JSON))
313315
assert [t["rule"]["id"] for t in loaded["triggers"]] == ["tst-037-008"]
314316
# other value must not be blocked
315317
with override_global_config(dict(_asm_enabled=True)), override_env(dict(DD_APPSEC_RULES=RULES_SRB)):
316318
_aux_appsec_prepare_tracer(tracer)
317-
resp = client.get("/", cookies={"keyname": "jdfoSDGFHappykivRG_234"})
318-
assert resp.status_code == 200
319+
resp = client.get("/test_cookies", cookies={"keyname": "jdfoSDGFHappykivRG_234"})
320+
assert resp.status_code == 201
321+
assert get_response_body(resp) == "OK Cookie"
319322
# appsec disabled must not block
320323
with override_global_config(dict(_asm_enabled=False)), override_env(dict(DD_APPSEC_RULES=RULES_SRB)):
321324
_aux_appsec_prepare_tracer(tracer, asm_enabled=False)
322-
resp = client.get("/", cookies={"keyname": "jdfoSDGFkivRG_234"})
323-
assert resp.status_code == 200
325+
resp = client.get("/test_cookies", cookies={"keyname": "jdfoSDGFkivRG_234"})
326+
assert resp.status_code == 201
327+
assert get_response_body(resp) == "OK Cookie"
324328

325329

326330
def test_request_suspicious_request_block_match_path_params(app, client, tracer, test_spans):

0 commit comments

Comments
 (0)