Repository navigation
Expand file tree
/
Copy pathheaders.go
More file actions
80 lines (71 loc) · 2.5 KB
/
Copy pathheaders.go
File metadata and controls
80 lines (71 loc) · 2.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package main
import (
"crypto/sha256"
"encoding/base64"
"io/fs"
"net/http"
"regexp"
"strings"
"sync"
)
// Security headers.
//
// Every response says it must not be framed (X-Frame-Options plus CSP
// frame-ancestors) and must not be MIME-sniffed. The console is never meant to
// be embedded in another page. Handlers that set a stricter policy of their own
// (the custom logo and sponsor images: "default-src 'none'; sandbox") replace
// the default one, which is what they want.
//
// The console pages get a full Content-Security-Policy. Their inline <script>
// blocks are allowed by SHA-256 hash, computed from the embedded file itself,
// so editing a page never leaves a stale hash behind. Inline styles stay
// allowed: the pages use style="" attributes and ECharts writes inline styles.
// The sponsor slot may fall back to a manifest on raw.githubusercontent.com
// whose images live on any https host, hence connect-src and img-src below.
const frameOnlyCSP = "frame-ancestors 'none'"
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := w.Header()
h.Set("X-Frame-Options", "DENY")
h.Set("X-Content-Type-Options", "nosniff")
h.Set("Referrer-Policy", "same-origin")
h.Set("Content-Security-Policy", frameOnlyCSP)
next.ServeHTTP(w, r)
})
}
// inlineScriptRe matches attribute-less <script> blocks: the only inline form the
// pages use. TestPageScriptsAllCovered keeps it that way.
var inlineScriptRe = regexp.MustCompile(`(?s)<script>(.*?)</script>`)
func inlineScriptHashes(page []byte) []string {
var out []string
for _, m := range inlineScriptRe.FindAllSubmatch(page, -1) {
sum := sha256.Sum256(m[1])
out = append(out, "'sha256-"+base64.StdEncoding.EncodeToString(sum[:])+"'")
}
return out
}
// pageCSP is the policy for one console page.
func pageCSP(page []byte) string {
script := append([]string{"'self'"}, inlineScriptHashes(page)...)
return strings.Join([]string{
"default-src 'self'",
"script-src " + strings.Join(script, " "),
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"connect-src 'self' https://raw.githubusercontent.com",
"object-src 'none'",
"base-uri 'none'",
"form-action 'self'",
frameOnlyCSP,
}, "; ")
}
var pageCSPs sync.Map // embedded page path → policy
func pageCSPFor(fsys fs.FS, name string) string {
if v, ok := pageCSPs.Load(name); ok {
return v.(string)
}
b, _ := fs.ReadFile(fsys, name)
csp := pageCSP(b)
pageCSPs.Store(name, csp)
return csp
}