oficina / home Public

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>
Paul Campbell pushed by paul@keymail.dev 099582ec4b06c610cecd03250ee532ac79cd4111 parent d39fc86
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)
}