Skip to content

Commit a1d3061

Browse files
committed
Commit
1 parent 39fb803 commit a1d3061

File tree

2 files changed

+73
-0
lines changed

2 files changed

+73
-0
lines changed

drift/instrumentation/django/html_utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,17 @@ def normalize_csrf_in_body(body: bytes | None) -> bytes | None:
6262
flags=re.IGNORECASE,
6363
)
6464

65+
# Pattern 3: JavaScript CSRF token assignment (e.g., DRF Swagger UI)
66+
# request.headers["X-CSRFTOKEN"] = "ABC123...";
67+
# Also handles single quotes and X-CSRFToken variants
68+
csrf_js_pattern = r'(headers\[["\']X-CSRFTOKEN["\']\]\s*=\s*["\'])[^"\']+(["\'])'
69+
body_str = re.sub(
70+
csrf_js_pattern,
71+
rf"\g<1>{CSRF_PLACEHOLDER}\2",
72+
body_str,
73+
flags=re.IGNORECASE,
74+
)
75+
6576
return body_str.encode("utf-8")
6677

6778
except Exception as e:

tests/unit/test_html_utils.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,68 @@ def test_handles_non_utf8_gracefully(self):
172172
result = normalize_csrf_in_body(html)
173173
assert result == html
174174

175+
def test_normalizes_js_csrf_header_assignment_double_quotes(self):
176+
"""JavaScript CSRF header assignment with double quotes should be normalized."""
177+
html = b"""<script>
178+
const requestInterceptor = (request) => {
179+
request.headers["X-CSRFTOKEN"] = "7180AeP5yScQ72FIE7z8tCtrrQc32Sgaqpqgk8ktVXeBXw7a5qDgXoGgLiWF22Ew";
180+
return request;
181+
};
182+
</script>"""
183+
result = normalize_csrf_in_body(html)
184+
assert result is not None
185+
assert CSRF_PLACEHOLDER.encode() in result
186+
assert b"7180AeP5yScQ72FIE7z8tCtrrQc32Sgaqpqgk8ktVXeBXw7a5qDgXoGgLiWF22Ew" not in result
187+
188+
def test_normalizes_js_csrf_header_assignment_single_quotes(self):
189+
"""JavaScript CSRF header assignment with single quotes should be normalized."""
190+
html = b"""<script>
191+
request.headers['X-CSRFTOKEN'] = 'someToken123';
192+
</script>"""
193+
result = normalize_csrf_in_body(html)
194+
assert result is not None
195+
assert CSRF_PLACEHOLDER.encode() in result
196+
assert b"someToken123" not in result
197+
198+
def test_normalizes_js_csrf_header_case_insensitive(self):
199+
"""JavaScript CSRF header matching should be case-insensitive (X-CSRFToken)."""
200+
html = b"""<script>
201+
request.headers["X-CSRFToken"] = "myToken456";
202+
</script>"""
203+
result = normalize_csrf_in_body(html)
204+
assert result is not None
205+
assert CSRF_PLACEHOLDER.encode() in result
206+
assert b"myToken456" not in result
207+
208+
def test_normalizes_js_csrf_header_preserves_surrounding_js(self):
209+
"""Surrounding JavaScript should be preserved when normalizing CSRF header."""
210+
html = b"""<script>
211+
const requestInterceptor = (request) => {
212+
if (!["GET", undefined].includes(request.method)) {
213+
request.headers["X-CSRFTOKEN"] = "tokenABC";
214+
}
215+
return request;
216+
};
217+
</script>"""
218+
result = normalize_csrf_in_body(html)
219+
assert result is not None
220+
assert b"requestInterceptor" in result
221+
assert b"return request" in result
222+
assert CSRF_PLACEHOLDER.encode() in result
223+
assert b"tokenABC" not in result
224+
225+
def test_normalizes_both_form_and_js_csrf_tokens(self):
226+
"""Both form hidden inputs and JS CSRF header assignments should be normalized."""
227+
html = b"""<html>
228+
<form><input name="csrfmiddlewaretoken" value="formToken123"></form>
229+
<script>request.headers["X-CSRFTOKEN"] = "jsToken456";</script>
230+
</html>"""
231+
result = normalize_csrf_in_body(html)
232+
assert result is not None
233+
assert result.count(CSRF_PLACEHOLDER.encode()) == 2
234+
assert b"formToken123" not in result
235+
assert b"jsToken456" not in result
236+
175237

176238
class TestNormalizeHtmlBody:
177239
"""Tests for normalize_html_body function."""

0 commit comments

Comments
 (0)