The script sweep reports the right reason for a single-quoted src
srcAttr matched only the double-quoted form, so `src='/x.js'` fell through to the "inline" branch. The guard still FAILED — fail-safe was the right default — but it named the wrong problem, and a guard that reports the wrong reason gets the wrong fix. It now accepts double quotes, single quotes and an unquoted value. Verified both ways: a single-quoted off-allowlist src on an allowlisted page now reports `loads "https://cdn.evil.invalid/x.js", which is not in ruling 21's allowlist` rather than claiming it is inline. Two things checked while here and found correct, recorded so nobody re-derives them: - chi.Walk enumerates all 20 mounted routes including the ones inside r.Group, so the CSP sweep really does cover /passkey/codes and /passkey/enrol and not just the top-level routes. - go test -race -count=2 is clean. The new keymail stub and the rate-limit tests are not order-dependent and leak nothing: each newTestApp builds its own Flow, so the limiter state is per-test, and the stub server is closed by t.Cleanup.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+19
−4
internal/home/noscript_test.go+19 −4
CI passed — run details
Intent
Attributed to this commit: 119 g CO₂e (14301858 tokens). This is an attribution, not a measurement — usage counted from this commit's own marker forward to the next one on step2/seal-and-secrets, never backward from it. A turn that produced more than one commit books all of its tokens to the last one, and a concurrent session on this branch can add to the same figure. Some of it was priced using the shared default rate, because no model-specific factor row exists yet for at least one model involved. The Carbon view has the repo total, which is never derived from this attribution.
diff --git a/internal/home/noscript_test.go b/internal/home/noscript_test.go| index abf53ff..827a507 100644 |
| --- a/internal/home/noscript_test.go |
| +++ b/internal/home/noscript_test.go |
| @@ -51,7 +51,13 @@ var scriptTag = regexp.MustCompile(`(?is)<script\b([^>]*)>(.*?)</\s*script\s*>`) |
| // skipped. |
| var scriptOpen = regexp.MustCompile(`(?is)<script\b`) |
| -var srcAttr = regexp.MustCompile(`(?i)\bsrc\s*=\s*"([^"]*)"`) |
| +// srcAttr accepts double quotes, single quotes and an unquoted value. |
| +// Only the double-quoted form was matched at first, so a |
| +// single-quoted src fell through to the "inline" branch — which still |
| +// FAILED the guard, since fail-safe was the right default, but with a |
| +// diagnosis naming the wrong problem. A guard that reports the wrong |
| +// reason gets the wrong fix. |
| +var srcAttr = regexp.MustCompile(`(?i)\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))`) |
| // scriptSrcs returns one entry per <script> in b: its src, or the empty |
| // string for an inline one. An inline body counts even when a src is |
| @@ -64,8 +70,14 @@ func scriptSrcs(b []byte) []string { |
| out = append(out, "") |
| continue |
| } |
| - if src := srcAttr.FindSubmatch(attrs); src != nil { |
| - out = append(out, string(src[1])) |
| + if m := srcAttr.FindSubmatch(attrs); m != nil { |
| + // One of the three alternatives matched; the others are empty. |
| + for _, g := range m[1:] { |
| + if len(g) > 0 { |
| + out = append(out, string(g)) |
| + break |
| + } |
| + } |
| continue |
| } |
| out = append(out, "") |
| @@ -184,9 +196,12 @@ func TestScriptSrcsSeesInlineAndSourced(t *testing.T) { |
| <script src="/static/x.js">alert(2)</script> |
| <script src="/static/spaced.js"></script > |
| <SCRIPT SRC="/static/shouty.js"></SCRIPT> |
| + <script src='/static/singly.js'></script> |
| + <script src=/static/bare.js></script> |
| `)) |
| want := []string{"/static/passkey.js", "", "/static/other.js", "", |
| - "/static/spaced.js", "/static/shouty.js"} |
| + "/static/spaced.js", "/static/shouty.js", |
| + "/static/singly.js", "/static/bare.js"} |
| if !slices.Equal(got, want) { |
| t.Errorf("scriptSrcs = %q, want %q", got, want) |
| } |