mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-05 04:16:31 +02:00
Merge remote-tracking branch 'remotes/from/ce/main'
This commit is contained in:
commit
524e02f814
3
changelog/_13303.txt
Normal file
3
changelog/_13303.txt
Normal file
@ -0,0 +1,3 @@
|
||||
```release-note:improvement
|
||||
ui: `unsafe-inline` is no longer included in the `style-src` CSP directive.
|
||||
```
|
||||
@ -6,6 +6,7 @@ package http
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@ -508,6 +509,13 @@ func wrapGenericHandler(core *vault.Core, h http.Handler, props *vault.HandlerPr
|
||||
|
||||
var hf func(w http.ResponseWriter, r *http.Request)
|
||||
hf = func(w http.ResponseWriter, r *http.Request) {
|
||||
// Generate CSP nonce once for this request
|
||||
cspNonce, err := generateCSPNonce()
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Errorf("failed to generate CSP nonce: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
// This block needs to be here so that upon sending SIGHUP, custom response
|
||||
// headers are also reloaded into the handlers.
|
||||
var customHeaders map[string][]*logical.CustomHeader
|
||||
@ -518,17 +526,19 @@ func wrapGenericHandler(core *vault.Core, h http.Handler, props *vault.HandlerPr
|
||||
customHeaders = listenerCustomHeaders.StatusCodeHeaderMap
|
||||
}
|
||||
}
|
||||
|
||||
// saving start time for the in-flight requests
|
||||
inFlightReqStartTime := time.Now()
|
||||
|
||||
nw := logical.NewStatusHeaderResponseWriter(w, customHeaders)
|
||||
nw := logical.NewStatusHeaderResponseWriter(w, customHeaders, cspNonce)
|
||||
|
||||
// Set the Cache-Control header for all the responses returned
|
||||
// by Vault
|
||||
nw.Header().Set("Cache-Control", "no-store")
|
||||
|
||||
// Start with the request context
|
||||
// Start with the request context and store the CSP nonce
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, cspNonceContextKey{}, cspNonce)
|
||||
var cancelFunc context.CancelFunc
|
||||
// Add our timeout, but not for the monitor or events endpoints, as they are streaming
|
||||
// Request URL path for sys/monitor looks like /v1/sys/monitor
|
||||
@ -845,6 +855,15 @@ func WrapForwardedForHandler(h http.Handler, l *configutil.Listener) http.Handle
|
||||
})
|
||||
}
|
||||
|
||||
// generateCSPNonce generates a cryptographically secure random nonce for CSP
|
||||
func generateCSPNonce() (string, error) {
|
||||
nonceBytes := make([]byte, 16)
|
||||
if _, err := rand.Read(nonceBytes); err != nil {
|
||||
return "", fmt.Errorf("failed to generate CSP nonce: %w", err)
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(nonceBytes), nil
|
||||
}
|
||||
|
||||
func handleUIHeaders(core *vault.Core, h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
header := w.Header()
|
||||
@ -855,8 +874,22 @@ func handleUIHeaders(core *vault.Core, h http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve CSP nonce from request context (generated in wrapGenericHandler)
|
||||
nonce, ok := req.Context().Value(cspNonceContextKey{}).(string)
|
||||
if !ok || nonce == "" {
|
||||
// Fallback: generate a new nonce if not found in context
|
||||
nonce, err = generateCSPNonce()
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for k := range userHeaders {
|
||||
v := userHeaders.Get(k)
|
||||
if k == "Content-Security-Policy" {
|
||||
v = logical.MergeNonceIntoCSP(v, nonce, "style-src")
|
||||
}
|
||||
header.Set(k, v)
|
||||
}
|
||||
|
||||
@ -864,16 +897,74 @@ func handleUIHeaders(core *vault.Core, h http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// cspNonceContextKey is used to store the CSP nonce in the request context
|
||||
type cspNonceContextKey struct{}
|
||||
|
||||
func handleUI(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
// The fileserver handler strips trailing slashes and does a redirect.
|
||||
// We don't want the redirect to happen so we preemptively trim the slash
|
||||
// here.
|
||||
req.URL.Path = strings.TrimSuffix(req.URL.Path, "/")
|
||||
|
||||
// For relevant HTML requests, wrap the response writer to inject the nonce
|
||||
hasExtension := strings.Contains(req.URL.Path, ".")
|
||||
if !hasExtension {
|
||||
nonce, ok := req.Context().Value(cspNonceContextKey{}).(string)
|
||||
if ok && nonce != "" {
|
||||
nrw := &nonceResponseWriter{
|
||||
ResponseWriter: w,
|
||||
nonce: nonce,
|
||||
}
|
||||
h.ServeHTTP(nrw, req)
|
||||
// Flush the buffered content after the handler completes
|
||||
nrw.flush()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
// nonceResponseWriter wraps http.ResponseWriter to inject CSP nonce into HTML
|
||||
type nonceResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
nonce string
|
||||
wroteHeader bool
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (nrw *nonceResponseWriter) Write(b []byte) (int, error) {
|
||||
if !nrw.wroteHeader {
|
||||
nrw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// Buffer the content
|
||||
nrw.buf.Write(b)
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (nrw *nonceResponseWriter) WriteHeader(statusCode int) {
|
||||
if nrw.wroteHeader {
|
||||
return
|
||||
}
|
||||
nrw.wroteHeader = true
|
||||
nrw.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func (nrw *nonceResponseWriter) flush() {
|
||||
// Inject nonce into buffered content
|
||||
htmlContent := nrw.buf.String()
|
||||
if htmlContent != "" && nrw.nonce != "" {
|
||||
nonceMetaTag := fmt.Sprintf(`<meta name="csp-nonce" content="%s">`, nrw.nonce)
|
||||
htmlContent = strings.Replace(htmlContent, "<head>", "<head>\n "+nonceMetaTag, 1)
|
||||
}
|
||||
|
||||
// Write the modified content
|
||||
nrw.ResponseWriter.Write([]byte(htmlContent))
|
||||
}
|
||||
|
||||
func handleUIStub() http.Handler {
|
||||
stubHTML := `
|
||||
<!DOCTYPE html>
|
||||
|
||||
@ -159,7 +159,22 @@ func testResponseHeader(t *testing.T, resp *http.Response, expectedHeaders map[s
|
||||
t.Helper()
|
||||
for k, v := range expectedHeaders {
|
||||
hv := resp.Header.Get(k)
|
||||
if v != hv {
|
||||
// Special handling for Content-Security-Policy header which may have a nonce appended
|
||||
if k == "Content-Security-Policy" {
|
||||
// Check if the actual header starts with the expected value
|
||||
// and optionally contains a nonce directive
|
||||
if !strings.HasPrefix(hv, v) {
|
||||
t.Fatalf("expected header value %v to start with %v, got %v", k, v, hv)
|
||||
}
|
||||
// Verify that if there's additional content, it's a valid nonce directive
|
||||
if len(hv) > len(v) {
|
||||
remainder := strings.TrimPrefix(hv, v)
|
||||
// Should start with ; and contain style-src 'nonce-
|
||||
if !strings.HasPrefix(remainder, ";style-src 'nonce-") {
|
||||
t.Fatalf("expected CSP header %v=%v to have nonce appended, got %v=%v", k, v, k, hv)
|
||||
}
|
||||
}
|
||||
} else if v != hv {
|
||||
t.Fatalf("expected header value %v=%v, got %v=%v", k, v, k, hv)
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/helper/wrapping"
|
||||
@ -267,14 +268,16 @@ type StatusHeaderResponseWriter struct {
|
||||
wroteHeader bool
|
||||
StatusCode int
|
||||
headers map[string][]*CustomHeader
|
||||
cspNonce string
|
||||
}
|
||||
|
||||
func NewStatusHeaderResponseWriter(w http.ResponseWriter, h map[string][]*CustomHeader) *StatusHeaderResponseWriter {
|
||||
func NewStatusHeaderResponseWriter(w http.ResponseWriter, h map[string][]*CustomHeader, cspNonce string) *StatusHeaderResponseWriter {
|
||||
return &StatusHeaderResponseWriter{
|
||||
wrapped: w,
|
||||
wroteHeader: false,
|
||||
StatusCode: 200,
|
||||
headers: h,
|
||||
cspNonce: cspNonce,
|
||||
}
|
||||
}
|
||||
|
||||
@ -332,7 +335,12 @@ func (w *StatusHeaderResponseWriter) setCustomResponseHeaders(status int) {
|
||||
// setter function to set the headers
|
||||
setter := func(hvl []*CustomHeader) {
|
||||
for _, hv := range hvl {
|
||||
w.Header().Set(hv.Name, hv.Value)
|
||||
headerValue := hv.Value
|
||||
// Inject CSP nonce if this is a Content-Security-Policy header and we have a nonce
|
||||
if hv.Name == "Content-Security-Policy" && w.cspNonce != "" {
|
||||
headerValue = MergeNonceIntoCSP(headerValue, w.cspNonce, "style-src")
|
||||
}
|
||||
w.Header().Set(hv.Name, headerValue)
|
||||
}
|
||||
}
|
||||
|
||||
@ -353,6 +361,37 @@ func (w *StatusHeaderResponseWriter) setCustomResponseHeaders(status int) {
|
||||
return
|
||||
}
|
||||
|
||||
// MergeNonceIntoCSP merges a nonce into the specified directive of a CSP policy
|
||||
func MergeNonceIntoCSP(cspPolicy, nonce string, targetDirective string) string {
|
||||
if cspPolicy == "" || nonce == "" {
|
||||
return cspPolicy
|
||||
}
|
||||
|
||||
nonceDirective := fmt.Sprintf("'nonce-%s'", nonce)
|
||||
directives := strings.Split(cspPolicy, ";")
|
||||
directiveFound := false
|
||||
|
||||
for i, directive := range directives {
|
||||
directive = strings.TrimSpace(directive)
|
||||
if strings.HasPrefix(directive, targetDirective) {
|
||||
directiveFound = true
|
||||
// Check if nonce is already present
|
||||
if !strings.Contains(directive, nonceDirective) {
|
||||
// Add nonce after the directive keyword
|
||||
directives[i] = directive + " " + nonceDirective
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If target directive doesn't exist, add it
|
||||
if !directiveFound {
|
||||
directives = append(directives, targetDirective+" "+nonceDirective)
|
||||
}
|
||||
|
||||
return strings.Join(directives, ";")
|
||||
}
|
||||
|
||||
var _ WrappingResponseWriter = &StatusHeaderResponseWriter{}
|
||||
|
||||
// ResolveRoleResponse returns a standard response to be returned by functions handling a ResolveRoleOperation
|
||||
|
||||
@ -10,13 +10,17 @@ module.exports = function (environment) {
|
||||
'font-src': ["'self'"],
|
||||
'connect-src': ["'self'"],
|
||||
'img-src': ["'self'", 'data:'],
|
||||
'style-src': ["'unsafe-inline'", "'self'"],
|
||||
'style-src': ["'self'"],
|
||||
'media-src': ["'self'"],
|
||||
'form-action': ["'none'"],
|
||||
};
|
||||
|
||||
policy['connect-src'].push('https://eu.i.posthog.com');
|
||||
|
||||
if (environment === 'test') {
|
||||
policy['style-src'].push("'unsafe-inline'");
|
||||
}
|
||||
|
||||
return {
|
||||
delivery: ['header', 'meta'],
|
||||
enabled: environment !== 'production',
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
{{else}}
|
||||
<Hds::CodeEditor
|
||||
@ariaLabel={{this.ariaLabel}}
|
||||
@cspNonce={{this.cspNonce}}
|
||||
@extraKeys={{@extraKeys}}
|
||||
@hasCopyButton={{true}}
|
||||
@hasFullScreenButton={{@hasFullScreenButton}}
|
||||
|
||||
@ -50,6 +50,12 @@ export default class JsonEditorComponent extends Component {
|
||||
return this.args.title ?? 'JSON Editor';
|
||||
}
|
||||
|
||||
get cspNonce() {
|
||||
// Read the CSP nonce from the meta tag injected by the server
|
||||
const metaTag = document.querySelector('meta[name="csp-nonce"]');
|
||||
return metaTag ? metaTag.getAttribute('content') : null;
|
||||
}
|
||||
|
||||
@action
|
||||
onSetup(editor) {
|
||||
this._codemirrorEditor = editor;
|
||||
|
||||
@ -43,7 +43,7 @@ func NewUIConfig(enabled bool, physicalStorage physical.Backend, barrierStorage
|
||||
if isHVD != "" && isHVD != "0" { // only if HVD, set connect-src to include posthog
|
||||
connectSrcHeader = "connect-src 'self' https://eu.i.posthog.com;"
|
||||
}
|
||||
defaultHeaders.Set("Content-Security-Policy", "default-src 'none'; "+connectSrcHeader+" img-src 'self' data:; script-src 'self'; style-src 'unsafe-inline' 'self'; form-action 'none'; frame-ancestors 'none'; font-src 'self'")
|
||||
defaultHeaders.Set("Content-Security-Policy", "default-src 'none'; "+connectSrcHeader+" img-src 'self' data:; script-src 'self'; style-src 'self'; form-action 'none'; frame-ancestors 'none'; font-src 'self'")
|
||||
return &UIConfig{
|
||||
physicalStorage: physicalStorage,
|
||||
barrierStorage: barrierStorage,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user