Files

109 lines
2.8 KiB
Go

package check
import (
"net/url"
"path/filepath"
"sort"
"strings"
)
// policy works out the Content-Security-Policy the built pages need: their
// own origin, the hosts they load scripts, styles, images and frames from,
// and 'unsafe-inline' only where a page still has inline code or styles. It
// can't see what scripts fetch at run time (connect-src), so a site adds
// those hosts itself.
func policy(pages []*page) string {
sets := map[string]map[string]bool{}
add := func(dir, src string) {
if sets[dir] == nil {
sets[dir] = map[string]bool{}
}
sets[dir][src] = true
}
origin := func(ref string) string {
u, err := url.Parse(ref)
if err != nil || u.Host == "" || (u.Scheme != "https" && u.Scheme != "http" && u.Scheme != "") {
return ""
}
scheme := u.Scheme
if scheme == "" {
scheme = "https"
}
return scheme + "://" + strings.ToLower(u.Host)
}
for _, pg := range pages {
if pg.redirect {
continue
}
for _, s := range pg.scripts {
if o := origin(s); o != "" {
add("script-src", o)
}
}
for _, s := range pg.styles {
if o := origin(s); o != "" {
add("style-src", o)
}
}
for _, s := range pg.images {
if strings.HasPrefix(s, "data:") {
add("img-src", "data:")
} else if o := origin(s); o != "" {
add("img-src", o)
}
}
for _, s := range pg.frames {
if o := origin(s); o != "" {
add("frame-src", o)
}
}
// A counter the consent script loads (only after consent): its own
// script, where it reports, and its tracking pixel.
for _, h := range pg.consentHosts {
add("script-src", h)
add("connect-src", h)
add("img-src", h)
}
if len(pg.inlineJS) > 0 || len(pg.handlers) > 0 {
add("script-src", "'unsafe-inline'")
}
if pg.inlineStyles > 0 {
add("style-src", "'unsafe-inline'")
}
}
list := func(dir string, base ...string) string {
var extra []string
for s := range sets[dir] {
extra = append(extra, s)
}
sort.Strings(extra)
return dir + " " + strings.Join(append(base, extra...), " ")
}
parts := []string{
"default-src 'self'",
list("script-src", "'self'"),
list("style-src", "'self'"),
list("img-src", "'self'"),
"font-src 'self'",
list("connect-src", "'self'"),
}
if len(sets["frame-src"]) > 0 {
parts = append(parts, list("frame-src"))
} else {
parts = append(parts, "frame-src 'none'")
}
parts = append(parts, "object-src 'none'", "base-uri 'self'", "form-action 'self'", "frame-ancestors 'self'")
return strings.Join(parts, "; ")
}
// Policy is the Content-Security-Policy for a built site, from its pages
// alone (for publishers that write it into a server config).
func Policy(out string) string {
c := &checker{out: out, siteDir: filepath.Dir(out)}
pages, err := c.readPages()
if err != nil {
return ""
}
return policy(pages)
}