Skip to main content

max / makenotwork

audit Run 15 Phase 5: update promo-code XSS test for delegated handlers The CSP migration replaced the promo list's inline writeText/onclick copy handler with a delegated data-action="copyText" + data-copy attribute. Update the test's layer-2 assertions to the new pattern: the code lives in escaped data-copy/data-arg2 attributes, and no inline onclick/writeText carries it. Security property unchanged (charset restriction + escaped data attribute). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-02 02:11 UTC
Commit: 5268535670e50ec8e018623f966953c19ead05f3
Parent: ee9853e
1 file changed, +11 insertions, -6 deletions
@@ -275,8 +275,9 @@ async fn discount_code_project_scoped() {
275 275 // UX-1: a promo code must never reach a JS-string context unescaped. Two layers
276 276 // now enforce this: (1) custom codes are charset-restricted at creation to
277 277 // [A-Z0-9_-], so a quote or HTML metacharacter can't enter in the first place;
278 - // (2) the code still renders into an escaped data-* attribute read by the copy
279 - // handler, never interpolated raw into a writeText('...') literal.
278 + // (2) the code renders only into escaped data-* attributes read by the delegated
279 + // copy/redemptions handlers, never into an inline on* handler or a writeText('...')
280 + // literal (the CSP now forbids inline script entirely).
280 281 #[tokio::test]
281 282 async fn promo_code_with_quote_is_rejected_and_valid_code_is_not_in_js_string_context() {
282 283 let mut h = TestHarness::new().await;
@@ -297,8 +298,8 @@ async fn promo_code_with_quote_is_rejected_and_valid_code_is_not_in_js_string_co
297 298 rejected.status, rejected.text
298 299 );
299 300
300 - // Layer 2: a valid code still lands in an escaped data-* attribute, and the
301 - // copy handler reads dataset rather than interpolating the code.
301 + // Layer 2: a valid code lands in escaped data-* attributes read by the
302 + // delegated handlers; no inline script carries the code.
302 303 let resp = h
303 304 .client
304 305 .htmx_post_form(
@@ -309,7 +310,11 @@ async fn promo_code_with_quote_is_rejected_and_valid_code_is_not_in_js_string_co
309 310 assert!(resp.status.is_success(), "create failed: {} {}", resp.status, resp.text);
310 311
311 312 let html = &resp.text;
312 - assert!(html.contains("writeText(this.dataset.code)"), "copy must read dataset: {html}");
313 + // The code is carried in an escaped data attribute for the delegated copy
314 + // handler (data-copy) and the redemptions handler (data-arg2), not inline JS.
315 + assert!(html.contains("data-copy=\"AB-CD\""), "code must live in a data-copy attr: {html}");
316 + assert!(html.contains("data-action=\"copyText\""), "copy must be a delegated data-action: {html}");
317 + // No inline script may carry the code: no writeText literal, no onclick.
313 318 assert!(!html.contains("writeText('"), "code must not be interpolated into a JS string literal");
314 - assert!(html.contains("data-code="), "code must live in a data-code attr: {html}");
319 + assert!(!html.contains("onclick"), "no inline onclick handler (CSP forbids inline script): {html}");
315 320 }