internal/server/web/csp.go
Ref: Size: 1.8 KiB History
package web
import (
"crypto/sha256"
"encoding/base64"
"io/fs"
"regexp"
"strings"
"sync"
)
// inlineScript matches a <script> element that carries its code inline. One with
// a src= attribute is skipped: it loads from 'self' and needs no hash.
var inlineScript = regexp.MustCompile(`(?s)<script([^>]*)>(.*?)</script>`)
var (
hashOnce sync.Once
hashes []string
)
// InlineScriptHashes returns CSP source tokens ("sha256-…") covering every
// inline script in the built index.html.
//
// The SPA's entry point is one inline block SvelteKit writes at build time, and
// its contents change on every build (it names a fresh global). Hashing the
// bytes actually embedded — rather than pinning a literal or giving up and
// allowing 'unsafe-inline' — keeps script-src strict across rebuilds with
// nothing for an operator to remember to update.
//
// A hash present in script-src is also what makes CSP ignore 'unsafe-inline',
// so this is the difference between a policy that blocks injected script and
// one that only looks like it does.
func InlineScriptHashes() []string {
hashOnce.Do(func() { hashes = scanInlineScripts(dist, "dist/index.html") })
return hashes
}
// scanInlineScripts is the testable half: it takes the filesystem and path so a
// test can hash a document it wrote itself. A missing or unreadable index.html
// yields no hashes — the UI is not built, and there is nothing to allow.
func scanInlineScripts(fsys fs.FS, path string) []string {
raw, err := fs.ReadFile(fsys, path)
if err != nil {
return nil
}
var out []string
for _, m := range inlineScript.FindAllSubmatch(raw, -1) {
if strings.Contains(strings.ToLower(string(m[1])), "src=") {
continue
}
if len(m[2]) == 0 {
continue
}
sum := sha256.Sum256(m[2])
out = append(out, "sha256-"+base64.StdEncoding.EncodeToString(sum[:]))
}
return out
}