1
0
Fork 0
mirror of https://github.com/Luzifer/go_helpers.git synced 2024-12-24 13:01:21 +00:00

Add CSP helper

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2023-06-17 14:37:18 +02:00
parent 6aae1097dd
commit fe36f52937
Signed by: luzifer
GPG key ID: D91C3E91E4CAD6F5
2 changed files with 126 additions and 0 deletions

93
http/csp.go Normal file
View file

@ -0,0 +1,93 @@
package http
import (
"encoding/base64"
"fmt"
"sort"
"strings"
)
type (
// CSP is a non-concurrency-safe map to hold a Content-Security-Policy
// and manipulate it afterwards to eventually render it into its
// header-representation
CSP map[string][]CSPSourceValue
// CSPHashAlgo defines the available hash algorithms
CSPHashAlgo string
// CSPSourceValue represents an value in the map for a given directive
CSPSourceValue string
)
// Collection of pre-defined values. For documentation see
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/Sources
const (
CSPHashSHA256 CSPHashAlgo = "sha256"
CSPHashSHA384 CSPHashAlgo = "sha384"
CSPHashSHA512 CSPHashAlgo = "sha512"
CSPSrcNone CSPSourceValue = "'none'"
CSPSrcReportSample CSPSourceValue = "'report-sample'"
CSPSrcSelf CSPSourceValue = "'self'"
CSPSrcStrictDynamic CSPSourceValue = "'strict-dynamic'"
CSPSrcUnsafeEval CSPSourceValue = "'unsafe-eval'"
CSPSrcUnsafeHashes CSPSourceValue = "'unsafe-hashes'"
CSPSrcUnsafeInline CSPSourceValue = "'unsafe-inline'"
CSPSrcWASMUnsafeEval CSPSourceValue = "'wasm-unsafe-eval'"
CSPSrcSchemeData CSPSourceValue = "data:"
CSPSrcSchemeMediastream CSPSourceValue = "mediastream:"
CSPSrcSchemeBlob CSPSourceValue = "blob:"
CSPSrcSchemeFilesystem CSPSourceValue = "filesystem:"
)
// CSPSrcHash takes an algo (sha256, sha384 or sha512) and the sum
// value and converts it into the right representation for the header
func CSPSrcHash(algo CSPHashAlgo, sum []byte) CSPSourceValue {
return CSPSourceValue(fmt.Sprintf("'%s-%s'", algo, base64.StdEncoding.EncodeToString(sum)))
}
// CSPSrcNonce takes a base64 encoded nonce value and converts it
// into the right representation for the header
func CSPSrcNonce(b64Value string) CSPSourceValue {
return CSPSourceValue(fmt.Sprintf("'nonce-%s'", b64Value))
}
// Add adds a single CSPSourceValue to the given directive
func (c CSP) Add(directive string, value CSPSourceValue) {
c[directive] = append(c[directive], value)
}
// Clone copies the CSP into a new one for manipulation
func (c CSP) Clone() CSP {
n := make(CSP)
for dir, vals := range c {
n[dir] = append([]CSPSourceValue{}, vals...)
}
return n
}
// Set replaces all present values for the given directive
func (c CSP) Set(directive string, value CSPSourceValue) {
c[directive] = []CSPSourceValue{value}
}
// ToHeaderValue encodes the policy into the format expected in the
// `Content-Security-Policy` header.
func (c CSP) ToHeaderValue() string {
var keys []string
for k := range c {
keys = append(keys, k)
}
sort.Strings(keys)
var parts []string
for _, k := range keys {
lst := []string{k}
for _, v := range c[k] {
lst = append(lst, string(v))
}
parts = append(parts, strings.Join(lst, " "))
}
return strings.Join(parts, ";")
}

33
http/csp_test.go Normal file
View file

@ -0,0 +1,33 @@
package http
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCSPClone(t *testing.T) {
c1 := CSP{"default-src": []CSPSourceValue{CSPSrcNone}}
c2 := c1.Clone()
c2.Add("default-src", CSPSrcSelf) // Makes no sense in real world!
assert.NotEqual(t, c1["default-src"], c2["default-src"], "expect c1 not to have changed")
}
func TestCSPToHeaderValue(t *testing.T) {
c := CSP{}
c.Set("default-src", CSPSrcNone)
c.Set("connect-src", CSPSrcSelf)
c.Set("font-src", CSPSrcSelf)
c.Set("img-src", CSPSrcSelf)
c.Add("img-src", CSPSrcSchemeData)
c.Set("script-src", CSPSrcSelf)
c.Add("script-src", CSPSrcUnsafeInline)
c.Set("style-src", CSPSrcSelf)
assert.Equal(t,
"connect-src 'self';default-src 'none';font-src 'self';img-src 'self' data:;script-src 'self' 'unsafe-inline';style-src 'self'",
c.ToHeaderValue())
}