The htmx login/registration partials answered success with an inline <script> that stashed the auth token and set window.location — refused by hash-mode script-src (no 'unsafe-inline'), so the user saw "Login successful! Redirecting..." and nothing else (reported from the home- page login form; /login was equally broken). The same pattern lived in collections.go (delete → redirect) and devices.go (token regen → reload, whose script tag was also unclosed). Replacement, per the AGENTS.md CSP rules: - auth.go login/registration: the partial now carries data-auth-token + data-redirect (redirect is path-only — open-redirect hygiene — and escaped); web/src/auth_forms.ts (new, armed from main.ts on every page) listens for htmx:afterSwap, stashes the token, and navigates. - collections.go delete: data-redirect, same listener. - devices.go token regen: htmx's native HX-Refresh response header. - The dead localStorage 'user'/'refreshToken' writes are dropped — no reader anywhere in web/src; only 'token' is load-bearing (19 modules). Tripwire extension: internal/middleware/csp_go_sources_test.go scans non-test Go sources for literal <script> tags and native on*= attributes (hand-written files only — templ codegen is owned by the .templ scan), verified to fire on a reintroduced probe. AGENTS.md CSP section notes the Go-emitted-partial vector. Verified live: home-page login form → /dashboard with token stashed and dashboard loading clean; /register form → /dashboard; collections and devices share the same mechanisms (data-redirect / HX-Refresh). Gates: make test (9 pkgs incl. both tripwires), vitest 155, Playwright 20, tsc/vp clean.
70 lines
2.2 KiB
Go
70 lines
2.2 KiB
Go
package middleware_test
|
|
|
|
import (
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Go-source CSP tripwire.
|
|
//
|
|
// The templ scan lives in templates/csp_source_test.go, but htmx partials
|
|
// can also be emitted from Go handler strings — auth.go used to answer a
|
|
// successful login with an inline <script>window.location…</script> that
|
|
// hash-mode CSP silently refused. This test scans the non-test Go sources
|
|
// for the same forbidden patterns: inline script tags and native on*=
|
|
// handler attributes in emitted HTML. Partials that need to navigate or
|
|
// stash state use data attributes + web/src/auth_forms.ts, or htmx's
|
|
// HX-Redirect/HX-Refresh headers.
|
|
|
|
var goScriptTagRe = regexp.MustCompile(`<script`)
|
|
|
|
var goHandlerAttrRe = regexp.MustCompile(
|
|
`on(click|error|submit|load|change|input|focus|blur|mouseover|keydown|keyup)\s*=\s*"`)
|
|
|
|
func TestGoSourcesAreCSPSafe(t *testing.T) {
|
|
root := "../.."
|
|
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() {
|
|
switch d.Name() {
|
|
case "node_modules", "static", ".git", "testdata", "docs":
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
name := d.Name()
|
|
if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
|
|
return nil
|
|
}
|
|
// Generated templ codegen is out of scope: its source of truth is
|
|
// the .templ file (scanned by templates/csp_source_test.go), and
|
|
// the codegen legitimately emits external <script src=…> tags.
|
|
if strings.HasSuffix(name, "_templ.go") {
|
|
return nil
|
|
}
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
src := string(raw)
|
|
for _, m := range goScriptTagRe.FindAllStringIndex(src, -1) {
|
|
t.Errorf("%s:%d: literal <script> in Go source — hash-mode CSP refuses inline scripts in emitted partials; use data attributes + web/src/auth_forms.ts or HX-Redirect/HX-Refresh",
|
|
path, 1+strings.Count(src[:m[0]], "\n"))
|
|
}
|
|
for _, m := range goHandlerAttrRe.FindAllStringIndex(src, -1) {
|
|
t.Errorf("%s:%d: native on*= handler attribute in Go source — CSP hash-mode refuses it; use Alpine bindings or delegated listeners",
|
|
path, 1+strings.Count(src[:m[0]], "\n"))
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|