Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
63 lines
2.1 KiB
Go
63 lines
2.1 KiB
Go
package web
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"net/http"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// inlineScript is a script element with its code in the page rather than
|
|
// in a src: what a policy names by its hash.
|
|
var inlineScript = regexp.MustCompile(`(?is)<script(\s[^>]*)?>(.*?)</script>`)
|
|
|
|
// contentSecurityPolicy is the policy for a rendered page: scripts from this
|
|
// server and the page's own inline ones, each by its hash, and nothing else
|
|
// — no inline handlers, no eval, no script a reply or a model card manages
|
|
// to put in the page. Styles may be inline (the pages and htmx use them),
|
|
// and fonts come from Google's for the public site.
|
|
//
|
|
// Hashed from the page as rendered, not from the templates: html/template
|
|
// drops comments inside scripts, so the template's text is not what the
|
|
// browser hashes.
|
|
func contentSecurityPolicy(page []byte) string {
|
|
var hashes []string
|
|
for _, m := range inlineScript.FindAllSubmatch(page, -1) {
|
|
attrs := strings.ToLower(string(m[1]))
|
|
if strings.Contains(attrs, "src=") || (strings.Contains(attrs, "type=") && !strings.Contains(attrs, "javascript")) {
|
|
continue // fetched, or data such as application/json
|
|
}
|
|
sum := sha256.Sum256(m[2])
|
|
h := "'sha256-" + base64.StdEncoding.EncodeToString(sum[:]) + "'"
|
|
if !slices.Contains(hashes, h) {
|
|
hashes = append(hashes, h)
|
|
}
|
|
}
|
|
return "default-src 'self'; " +
|
|
"script-src 'self'" + prefixSpace(strings.Join(hashes, " ")) + "; " +
|
|
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
|
|
"font-src 'self' https://fonts.gstatic.com; " +
|
|
"img-src 'self' data: https:; " + // model cards show their images
|
|
"connect-src 'self'; " +
|
|
"object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'"
|
|
}
|
|
|
|
func prefixSpace(s string) string {
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
return " " + s
|
|
}
|
|
|
|
// securePage sets the headers every page is served with, and writes it.
|
|
func securePage(h http.Header, page *bytes.Buffer) {
|
|
h.Set("Content-Security-Policy", contentSecurityPolicy(page.Bytes()))
|
|
h.Set("X-Content-Type-Options", "nosniff")
|
|
if h.Get("Referrer-Policy") == "" {
|
|
h.Set("Referrer-Policy", "same-origin")
|
|
}
|
|
}
|