Skip to content
This repository has been archived by the owner on Dec 8, 2020. It is now read-only.

Commit

Permalink
Update: Adds Content-Security-Policy middleware builder
Browse files Browse the repository at this point in the history
  • Loading branch information
kyleterry committed Jun 18, 2020
1 parent 321b227 commit f0b5949
Show file tree
Hide file tree
Showing 2 changed files with 167 additions and 0 deletions.
132 changes: 132 additions & 0 deletions httputil/api/csp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package api

import (
"net/http"
"strings"
)

var (
cspQuotableSources = map[string]struct{}{
"none": {},
"self": {},
"unsafe-eval": {},
"unsafe-hashes": {},
"unsafe-inline": {},
"strict-dynamic": {},
"report-sample": {},
}
)

func cspQuoteSources(sources []string) []string {
var newSources = make([]string, len(sources))

for i := range sources {
src := sources[i]
if _, ok := cspQuotableSources[sources[i]]; ok {
src = "'" + sources[i] + "'"
}

newSources[i] = src
}

return newSources
}

type cspDirectiveType int

// This is an incomplete list of directives for the
// Content-Security-Policy header. More directives can
// be added here for further CSP support.
const (
CSPBaseURI cspDirectiveType = iota
CSPBlockAllMixedContent
CSPDefaultSrc
CSPFontSrc
CSPFormSrc
CSPFrameAncestors
CSPFrameSrc
CSPManifestSrc
CSPMediaSrc
CSPScriptSrc
CSPStyleSrc
CSPImgSrc
)

var cspMapping = map[cspDirectiveType]func(sources []string) string{
CSPBaseURI: baseSourceDirectiveFactory("base-uri"),
CSPBlockAllMixedContent: func(_ []string) string {
return "block-all-mixed-content"
},
CSPDefaultSrc: baseSourceDirectiveFactory("default-src"),
CSPFontSrc: baseSourceDirectiveFactory("font-src"),
CSPFormSrc: baseSourceDirectiveFactory("form-src"),
CSPFrameAncestors: baseSourceDirectiveFactory("frame-ancestors"),
CSPFrameSrc: baseSourceDirectiveFactory("frame-src"),
CSPManifestSrc: baseSourceDirectiveFactory("manifest-src"),
CSPMediaSrc: baseSourceDirectiveFactory("media-src"),
CSPScriptSrc: baseSourceDirectiveFactory("script-src"),
CSPStyleSrc: baseSourceDirectiveFactory("style-src"),
CSPImgSrc: baseSourceDirectiveFactory("img-src"),
}

func baseSourceDirectiveFactory(directiveName string) func([]string) string {
return func(sources []string) string {
sources = cspQuoteSources(sources)

s := []string{directiveName}
s = append(s, sources...)

return strings.Join(s, " ")
}
}

// CSPBuilder uses values stored for src's and builds a valid
// Content-Security-Policy header.
type CSPBuilder struct {
directives map[cspDirectiveType]string
order []cspDirectiveType
}

// SetDirective sets sources for directive types. Sources is a variadic and not every
// directive type uses them, so they are not always required.
func (cb *CSPBuilder) SetDirective(dt cspDirectiveType, sources ...string) *CSPBuilder {
if cb.directives == nil {
cb.directives = make(map[cspDirectiveType]string)
}

result := cspMapping[dt](sources)

if _, ok := cb.directives[dt]; !ok {
cb.order = append(cb.order, dt)
}

cb.directives[dt] = result

return cb
}

// Middleware is a middleware wrapper that can be used to inject a
// Content-Security-Policy header into a server's response. It builds
// the header value and joins it together in the proper format then passes
// the request off to the `next` http.Handler.
func (cb *CSPBuilder) Middleware(next http.Handler) http.Handler {
var policy string

if len(cb.directives) > 0 {
directives := []string{}

for _, d := range cb.order {
directives = append(directives, cb.directives[d])
}

policy = strings.Join(directives, "; ")
}

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if policy != "" {
w.Header().Set("content-security-policy", policy)
}

next.ServeHTTP(w, r)
})
}
35 changes: 35 additions & 0 deletions httputil/api/csp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package api

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/require"
)

func TestCSPBuilder(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})

builder := CSPBuilder{}
builder.SetDirective(CSPDefaultSrc, "self").
SetDirective(CSPScriptSrc, "self", "*.example.com").
SetDirective(CSPMediaSrc, "none").
SetDirective(CSPImgSrc, "example-bucket-gGi7b2.s3.amazonaws.com").
SetDirective(CSPBlockAllMixedContent)

wrapped := builder.Middleware(handler)

req, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
require.NoError(t, err)

resp := httptest.NewRecorder()

wrapped.ServeHTTP(resp, req)
result := resp.Result()

require.Equal(t, http.StatusOK, result.StatusCode)

csp := result.Header.Get("content-security-policy")
require.Equal(t, "default-src 'self'; script-src 'self' *.example.com; media-src 'none'; img-src example-bucket-gGi7b2.s3.amazonaws.com; block-all-mixed-content", csp)
}

0 comments on commit f0b5949

Please sign in to comment.