| // Copyright 2020 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| // csphash computes the hashes of script tags in files, | |
| // and checks that they are added to our content | |
| // security policy. | |
| package main | |
| import ( | |
| "bytes" | |
| "crypto/sha256" | |
| "encoding/base64" | |
| "flag" | |
| "fmt" | |
| "log" | |
| "os" | |
| "regexp" | |
| ) | |
| var hashFile = flag.String("hf", "internal/middleware/secureheaders.go", "file with hashes for CSP header") | |
| func main() { | |
| flag.Usage = func() { | |
| fmt.Fprintf(flag.CommandLine.Output(), "usage: %s [flags] FILES\n", os.Args[0]) | |
| fmt.Fprintf(flag.CommandLine.Output(), "suggestion for FILES: content/static/html/**/*.tmpl\n") | |
| flag.PrintDefaults() | |
| } | |
| flag.Parse() | |
| if flag.NArg() == 0 { | |
| flag.Usage() | |
| os.Exit(1) | |
| } | |
| cspHashes, err := extractHashes(*hashFile) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| cspHashMap := map[string]bool{} | |
| for _, h := range cspHashes { | |
| cspHashMap[h] = true | |
| } | |
| ok := true | |
| for _, file := range flag.Args() { | |
| scripts, err := scripts(file) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| for _, s := range scripts { | |
| if bytes.Contains(s.tag, []byte("src=")) { | |
| fmt.Printf("%s: has script with src attribute: %s\n", file, s.tag) | |
| ok = false | |
| } | |
| if bytes.Contains(s.body, []byte("{{")) { | |
| fmt.Printf("%s: has script with template expansion:\n%s\n", file, s.body) | |
| fmt.Printf("Scripts must be static so they have a constant hash.\n") | |
| ok = false | |
| continue | |
| } | |
| hash := cspHash(s.body) | |
| if !cspHashMap[hash] { | |
| fmt.Printf("missing hash: add the lines below to %s:\n", *hashFile) | |
| fmt.Printf(" // From %s\n", file) | |
| fmt.Printf(` "'sha256-%s'",`, hash) | |
| fmt.Println() | |
| ok = false | |
| } else { | |
| delete(cspHashMap, hash) | |
| } | |
| } | |
| } | |
| for h := range cspHashMap { | |
| fmt.Printf("unused hash %s\n", h) | |
| ok = false | |
| } | |
| if !ok { | |
| fmt.Printf("Add missing hashes to %s and remove unused ones.\n", *hashFile) | |
| os.Exit(1) | |
| } | |
| } | |
| var hashRegexp = regexp.MustCompile(`'sha256-([^']+)'`) | |
| // extractHashes scans the given file for CSP-style hashes and returns them. | |
| func extractHashes(filename string) ([]string, error) { | |
| contents, err := os.ReadFile(filename) | |
| if err != nil { | |
| return nil, err | |
| } | |
| var hashes []string | |
| matches := hashRegexp.FindAllSubmatch(contents, -1) | |
| for _, m := range matches { | |
| hashes = append(hashes, string(m[1])) | |
| } | |
| return hashes, nil | |
| } | |
| func cspHash(b []byte) string { | |
| h := sha256.Sum256(b) | |
| return base64.StdEncoding.EncodeToString(h[:]) | |
| } | |
| // script represents an HTML script element. | |
| type script struct { | |
| tag []byte // `<script attr="a"...>` | |
| body []byte // text between open and close script tags | |
| } | |
| // scripts returns all the script elements in the given file. | |
| func scripts(filename string) ([]*script, error) { | |
| contents, err := os.ReadFile(filename) | |
| if err != nil { | |
| return nil, fmt.Errorf("%s: %v", filename, err) | |
| } | |
| return scriptsReader(contents) | |
| } | |
| var ( | |
| // Assume none of the attribute values contain a '>'. | |
| // Regexp flag `i` means case-insensitive. | |
| scriptStartRegexp = regexp.MustCompile(`(?i:<script>|<script\s[^>]*>)`) | |
| // Assume all scripts end with a full close tag. | |
| scriptEndRegexp = regexp.MustCompile(`(?i:</script>)`) | |
| ) | |
| func scriptsReader(b []byte) ([]*script, error) { | |
| var scripts []*script | |
| offset := 0 | |
| for { | |
| start := scriptStartRegexp.FindIndex(b) | |
| if start == nil { | |
| return scripts, nil | |
| } | |
| tag := b[start[0]:start[1]] | |
| b = b[start[1]:] | |
| offset += start[1] | |
| end := scriptEndRegexp.FindIndex(b) | |
| if end == nil { | |
| return nil, fmt.Errorf("%s is missing an end tag", tag) | |
| } | |
| scripts = append(scripts, &script{ | |
| tag: tag, | |
| body: b[:end[0]], | |
| }) | |
| b = b[end[1]:] | |
| offset += end[1] | |
| } | |
| } |