mirror of
https://github.com/Luzifer/go_helpers.git
synced 2024-12-25 13:31:21 +00:00
Add CSP helper
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
6aae1097dd
commit
fe36f52937
2 changed files with 126 additions and 0 deletions
93
http/csp.go
Normal file
93
http/csp.go
Normal 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
33
http/csp_test.go
Normal 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())
|
||||||
|
}
|
Loading…
Reference in a new issue