feat(daemon): csp nonce handling (#6587)

* Adding nonce handling

* Nonce token and fix content length mismatch

* Updating nonce injection for override situations

* nonce handling in Content Security Policy for custom CSP scenarios

* return original length in cspWriter to prevent truncation
This commit is contained in:
Bharath Gajjala authored and GitHub committed 2026-04-27 09:06:46 -05:00
1 parent af937ba6b1
commit bf24d6607f
4 files changed
+239 -2

No files matched your search

@@ -12,6 +12,7 @@ import (
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
@@ -19,6 +20,7 @@ import (
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/go-secure-stdlib/listenerutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/genproto/googleapis/api/httpbody"
@@ -467,3 +469,98 @@ func TestGetActions(t *testing.T) {
})
}
}
func TestWrapHandlerWithCsp(t *testing.T) {
defaultCsp := "default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; frame-src 'self'; font-src 'self'; connect-src 'self'; img-src 'self' data:; style-src 'self'; media-src 'self'; manifest-src 'self'; style-src-attr 'self'; frame-ancestors 'self'"
newProps := func(csp string) HandlerProperties {
if csp == "" {
csp = defaultCsp
}
headers := http.Header{}
headers.Set("Content-Security-Policy", csp)
return HandlerProperties{
ListenerConfig: &listenerutil.ListenerConfig{
CustomUiResponseHeaders: map[int]http.Header{
0: headers,
},
},
}
}
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
testCases := []struct {
name string
path string
isUi bool
customCsp string
wantNonce bool
wantDefaultCsp bool
wantContains []string
}{
{
name: "document request gets nonce",
path: "/",
isUi: true,
wantNonce: true,
},
{
name: "static asset gets default CSP without nonce",
path: "/assets/app.js",
isUi: true,
wantDefaultCsp: true,
wantNonce: false,
},
{
name: "non UI request gets no CSP header",
path: "/",
isUi: false,
wantNonce: false,
},
{
name: "custom CSP without style-src gets style-src created",
path: "/",
isUi: true,
customCsp: "default-src 'none'; script-src 'self'",
wantContains: []string{"default-src 'none'", "script-src 'self'", "; style-src 'nonce-", "'self'"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
assert := assert.New(t)
isUiFunc := func(*http.Request) bool { return tc.isUi }
handler := wrapHandlerWithCsp(inner, newProps(tc.customCsp), isUiFunc)
req := httptest.NewRequest("GET", tc.path, nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
csp := rec.Header().Get("Content-Security-Policy")
if len(tc.wantContains) > 0 {
require.NotEmpty(csp)
for _, want := range tc.wantContains {
assert.Contains(csp, want)
}
return
}
switch {
case tc.wantNonce:
require.NotEmpty(csp)
assert.Contains(csp, "'nonce-")
case tc.wantDefaultCsp:
require.NotEmpty(csp)
assert.NotContains(csp, "'nonce-")
require.Equal(defaultCsp, csp)
default:
require.Empty(csp)
}
})
}
}