VYPR
High severity7.1GHSA Advisory· Published Jun 8, 2026· Updated Jun 8, 2026

nebula-mesh: Web UI and API responses lack security headers (CSP, X-Frame-Options, HSTS, etc.)

CVE-2026-47723

Description

None of the response paths in internal/web/ or internal/api/ set the standard browser-security headers. grep for Content-Security-Policy, X-Frame-Options, Strict-Transport-Security, X-Content-Type-Options, Referrer-Policy returns zero matches across the codebase.

Impact

The admin UI signs CA certificates, mints API keys (returned inline once per page), displays TOTP QR codes, and exposes operator-management forms. Missing X-Frame-Options: DENY / frame-ancestors 'none' is a real clickjacking lever against an admin browsing /ui/operators/* or /ui/cas/*. Missing X-Content-Type-Options: nosniff allows MIME confusion on any user-supplied content surface. Missing HSTS on TLS deployments leaves a downgrade window.

Affected

All released versions up to v0.3.0.

Suggested fix

A single response-header middleware mounted at the chi router root in both /ui/* and /api/* paths:

func securityHeadersMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
        h := rw.Header()
        h.Set("Content-Security-Policy",
            "default-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'")
        h.Set("X-Content-Type-Options", "nosniff")
        h.Set("Referrer-Policy", "same-origin")
        h.Set("X-Frame-Options", "DENY")  // belt-and-braces; CSP frame-ancestors is the modern path
        if r.TLS != nil {
            h.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
        }
        next.ServeHTTP(rw, r)
    })
}

The inline ` in layout.html for CSRF wiring (added in the CSRF advisory) will need either a nonce, a hash in CSP, or external-file extraction. Easiest path: a nonce per request (crypto/rand, base64) injected into both the CSP header and the script's nonce=""` attribute.

Affected products

1

Patches

1
b45fda5476c4

fix(http): add security response headers middleware (#125)

https://github.com/forgekeep/nebula-meshEvsyukov DenisMay 20, 2026via ghsa-ref
3 files changed · +144 1
  • internal/cli/security_headers.go+32 0 added
    @@ -0,0 +1,32 @@
    +package cli
    +
    +import "net/http"
    +
    +// securityHeaders wraps next with HTTP response security headers per advisory
    +// GHSA-w7w5-5gcp-38rw: clickjacking, MIME sniffing, referrer leakage, and
    +// (on TLS) protocol downgrade defenses.
    +//
    +// CSP keeps 'unsafe-inline' on script-src and style-src for compatibility
    +// with existing inline <script>, <style>, onclick=, and style="..." usage
    +// in templates. Tightening that requires extracting inline blocks and
    +// rewiring inline event handlers, tracked separately.
    +func securityHeaders(next http.Handler) http.Handler {
    +	const csp = "default-src 'self'; " +
    +		"script-src 'self' 'unsafe-inline'; " +
    +		"style-src 'self' 'unsafe-inline'; " +
    +		"img-src 'self' data:; " +
    +		"frame-ancestors 'none'; " +
    +		"base-uri 'none'; " +
    +		"form-action 'self'"
    +	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    +		h := w.Header()
    +		h.Set("Content-Security-Policy", csp)
    +		h.Set("X-Content-Type-Options", "nosniff")
    +		h.Set("Referrer-Policy", "same-origin")
    +		h.Set("X-Frame-Options", "DENY")
    +		if r.TLS != nil {
    +			h.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
    +		}
    +		next.ServeHTTP(w, r)
    +	})
    +}
    
  • internal/cli/security_headers_test.go+111 0 added
    @@ -0,0 +1,111 @@
    +package cli
    +
    +import (
    +	"crypto/tls"
    +	"net/http"
    +	"net/http/httptest"
    +	"strings"
    +	"testing"
    +)
    +
    +func TestSecurityHeaders_PlainHTTP(t *testing.T) {
    +	inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    +		w.WriteHeader(http.StatusOK)
    +	})
    +	h := securityHeaders(inner)
    +
    +	rec := httptest.NewRecorder()
    +	req := httptest.NewRequest(http.MethodGet, "/anything", nil)
    +	h.ServeHTTP(rec, req)
    +
    +	got := rec.Result().Header
    +
    +	wantPrefix := map[string]string{
    +		"Content-Security-Policy": "default-src 'self';",
    +		"X-Content-Type-Options":  "nosniff",
    +		"Referrer-Policy":         "same-origin",
    +		"X-Frame-Options":         "DENY",
    +	}
    +	for k, prefix := range wantPrefix {
    +		v := got.Get(k)
    +		if v == "" {
    +			t.Errorf("%s missing", k)
    +			continue
    +		}
    +		if !strings.HasPrefix(v, prefix) {
    +			t.Errorf("%s = %q, want prefix %q", k, v, prefix)
    +		}
    +	}
    +
    +	// HSTS must NOT be set on plain HTTP — sending it from a plain
    +	// listener would invite the client to upgrade to a non-listening
    +	// HTTPS endpoint.
    +	if v := got.Get("Strict-Transport-Security"); v != "" {
    +		t.Errorf("HSTS set on plain HTTP: %q", v)
    +	}
    +}
    +
    +func TestSecurityHeaders_TLS_SetsHSTS(t *testing.T) {
    +	inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    +		w.WriteHeader(http.StatusOK)
    +	})
    +	h := securityHeaders(inner)
    +
    +	rec := httptest.NewRecorder()
    +	req := httptest.NewRequest(http.MethodGet, "/anything", nil)
    +	req.TLS = &tls.ConnectionState{}
    +	h.ServeHTTP(rec, req)
    +
    +	hsts := rec.Result().Header.Get("Strict-Transport-Security")
    +	if hsts == "" {
    +		t.Fatal("HSTS missing on TLS request")
    +	}
    +	if !strings.Contains(hsts, "max-age=") {
    +		t.Errorf("HSTS without max-age: %q", hsts)
    +	}
    +	if !strings.Contains(hsts, "includeSubDomains") {
    +		t.Errorf("HSTS without includeSubDomains: %q", hsts)
    +	}
    +}
    +
    +func TestSecurityHeaders_CSPDirectives(t *testing.T) {
    +	inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    +		w.WriteHeader(http.StatusOK)
    +	})
    +	h := securityHeaders(inner)
    +
    +	rec := httptest.NewRecorder()
    +	req := httptest.NewRequest(http.MethodGet, "/", nil)
    +	h.ServeHTTP(rec, req)
    +
    +	csp := rec.Result().Header.Get("Content-Security-Policy")
    +	want := []string{
    +		"default-src 'self'",
    +		"frame-ancestors 'none'",
    +		"base-uri 'none'",
    +		"form-action 'self'",
    +	}
    +	for _, d := range want {
    +		if !strings.Contains(csp, d) {
    +			t.Errorf("CSP missing %q; got %q", d, csp)
    +		}
    +	}
    +}
    +
    +func TestSecurityHeaders_AppliedOnErrorResponse(t *testing.T) {
    +	inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    +		http.Error(w, "boom", http.StatusInternalServerError)
    +	})
    +	h := securityHeaders(inner)
    +
    +	rec := httptest.NewRecorder()
    +	req := httptest.NewRequest(http.MethodGet, "/", nil)
    +	h.ServeHTTP(rec, req)
    +
    +	if rec.Code != http.StatusInternalServerError {
    +		t.Fatalf("status = %d, want 500", rec.Code)
    +	}
    +	if rec.Result().Header.Get("X-Frame-Options") != "DENY" {
    +		t.Error("security headers stripped on error response")
    +	}
    +}
    
  • internal/cli/serve.go+1 1 modified
    @@ -227,7 +227,7 @@ func Serve(configPath string) error {
     
     	httpServer := &http.Server{
     		Addr:         cfg.Listen,
    -		Handler:      mux,
    +		Handler:      securityHeaders(mux),
     		ReadTimeout:  10 * time.Second,
     		WriteTimeout: 10 * time.Second,
     	}
    

Vulnerability mechanics

Synthesis attempt was rejected by the grounding validator. Re-run pending.

References

4

News mentions

0

No linked articles in our index yet.