Skip to content

Commit

Permalink
Dynamic Csp nonce (#26)
Browse files Browse the repository at this point in the history
* Dynamic Csp nonce

1. Package level Nonce function to return nonce associated with the present request wuing the context package added in Go 1.7
2. Transparent implementaion using the text/template package for nonce in csp

* Travis Changes

* Removed byte size setting, defaults to 16, changed Nonce function to CSP Nonce, Using cryto/rand reader to generate nonce

* Review Changes

* Changed all CSP Nonce related code a separeate file
* Removed the shallow copy *r = *r.WithContext()
* Updated tests with the new changes
* Added withCSPNonce functions
* Renamed CSP related fucntions and variables to explicitly start with CSP

* Improved Error Message, Formatting
  • Loading branch information
srikrsna authored and unrolled committed Jan 5, 2018
1 parent 0f73fc7 commit a8e1b2a
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 3 deletions.
2 changes: 0 additions & 2 deletions .travis.yml
@@ -1,8 +1,6 @@
language: go

go:
- 1.5.x
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
Expand Down
36 changes: 36 additions & 0 deletions csp.go
@@ -0,0 +1,36 @@
package secure

import (
"context"
"crypto/rand"
"encoding/base64"
"io"
"net/http"
)

// CSPNonce returns the nonce value associated with the present request. If no nonce has been generated it returns an empty string.
func CSPNonce(c context.Context) string {
if val, ok := c.Value(cspNonceKey).(string); ok {
return val
}

return ""
}

type key int

const cspNonceKey key = iota

func cspRandNonce() string {
var buf [cspNonceSize]byte
_, err := io.ReadFull(rand.Reader, buf[:])
if err != nil {
panic("CSP Nonce rand.Reader failed" + err.Error())
}

return base64.RawStdEncoding.EncodeToString(buf[:])
}

func withCSPNonce(r *http.Request, nonce string) *http.Request {
return r.WithContext(context.WithValue(r.Context(), cspNonceKey, nonce))
}
41 changes: 41 additions & 0 deletions csp_test.go
@@ -0,0 +1,41 @@
package secure

import (
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
)

func TestCSPNonce(t *testing.T) {
s := New(Options{
ContentSecurityPolicy: "default-src 'self' $NONCE; script-src 'strict-dynamic' $NONCE",
})

res := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/foo", nil)

s.Handler(myHandler).ServeHTTP(res, req)

expect(t, res.Code, http.StatusOK)

csp := res.Header().Get("Content-Security-Policy")
expect(t, strings.Count(csp, "'nonce-"), 2)

nonce := strings.Split(strings.Split(csp, "'")[3], "-")[1]

_, err := base64.RawStdEncoding.DecodeString(nonce)
expect(t, err, nil)

expect(t, csp, fmt.Sprintf("default-src 'self' 'nonce-%[1]s'; script-src 'strict-dynamic' 'nonce-%[1]s'", nonce))
}

func TestWithCSPNonce(t *testing.T) {
req, _ := http.NewRequest("GET", "/foo", nil)

nonce := "jdgKGHkbnd+/"

expect(t, CSPNonce(withCSPNonce(req, nonce).Context()), nonce)
}
20 changes: 19 additions & 1 deletion secure.go
Expand Up @@ -19,6 +19,8 @@ const (
cspHeader = "Content-Security-Policy"
hpkpHeader = "Public-Key-Pins"
referrerPolicyHeader = "Referrer-Policy"

cspNonceSize = 16
)

func defaultBadHostHandler(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -58,6 +60,8 @@ type Options struct {
// CustomBrowserXssValue allows the X-XSS-Protection header value to be set with a custom value. This overrides the BrowserXssFilter option. Default is "".
CustomBrowserXssValue string
// ContentSecurityPolicy allows the Content-Security-Policy header value to be set with a custom value. Default is "".
// Passing a template string will replace `$NONCE` with a dynamic nonce value of 16 bytes for each request which can be later retrieved using the Nonce function.
// Eg: script-src $NONCE -> script-src 'nonce-a2ZobGFoZg=='
ContentSecurityPolicy string
// PublicKey implements HPKP to prevent MITM attacks with forged certificates. Default is "".
PublicKey string
Expand All @@ -66,6 +70,8 @@ type Options struct {
// When developing, the AllowedHosts, SSL, and STS options can cause some unwanted effects. Usually testing happens on http, not https, and on localhost, not your production domain... so set this to true for dev environment.
// If you would like your development environment to mimic production with complete Host blocking, SSL redirects, and STS headers, leave this as false. Default if false.
IsDevelopment bool

nonceEnabled bool
}

// Secure is a middleware that helps setup a few basic security features. A single secure.Options struct can be
Expand All @@ -87,6 +93,10 @@ func New(options ...Options) *Secure {
o = options[0]
}

o.ContentSecurityPolicy = strings.Replace(o.ContentSecurityPolicy, "$NONCE", "'nonce-%[1]s'", -1)

o.nonceEnabled = strings.Contains(o.ContentSecurityPolicy, "%[1]s")

return &Secure{
opt: o,
badHostHandler: http.HandlerFunc(defaultBadHostHandler),
Expand All @@ -101,6 +111,10 @@ func (s *Secure) SetBadHostHandler(handler http.Handler) {
// Handler implements the http.HandlerFunc for integration with the standard net/http lib.
func (s *Secure) Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if s.opt.nonceEnabled {
r = withCSPNonce(r, cspRandNonce())
}

// Let secure process the request. If it returns an error,
// that indicates the request should not continue.
err := s.Process(w, r)
Expand Down Expand Up @@ -222,7 +236,11 @@ func (s *Secure) Process(w http.ResponseWriter, r *http.Request) error {

// Content Security Policy header.
if len(s.opt.ContentSecurityPolicy) > 0 {
w.Header().Add(cspHeader, s.opt.ContentSecurityPolicy)
if s.opt.nonceEnabled {
w.Header().Add(cspHeader, fmt.Sprintf(s.opt.ContentSecurityPolicy, CSPNonce(r.Context())))
} else {
w.Header().Add(cspHeader, s.opt.ContentSecurityPolicy)
}
}

// Referrer Policy header.
Expand Down

0 comments on commit a8e1b2a

Please sign in to comment.