security: fix open redirect and add small hardening (#1227)
## What this does Fixes an open redirect plus a handful of small, mostly-invisible hardening changes around it. ## What's in it - **The fix**: trailing-slash redirects no longer let `//evil.com/` redirect off-host. - **SSRF block** on the DNS/HTTP namespace verification — refuses to dial loopback, RFC1918, link-local, multicast, CGNAT, or IP-literal/single-label \"domains\". - **Slow-client guard**: `ReadTimeout` and `IdleTimeout` on the HTTP server (no `WriteTimeout` — would risk cutting off legitimate multi-package publishes). - **Defence-in-depth headers** on the UI (`/`) and 404 responses. - **5xx responses** stop leaking raw DB error text to clients (logged server-side instead). - **Search filter** stops treating `%` and `_` as wildcards. - **Validators**: PyPI/NuGet identifiers URL-escaped before fetching; MCPB no longer follows redirects. - **Misc**: `Version` field bounded to 255 chars; logs quote user input; `BlockedNamespaces` denylist now catches subdomain claimants. Each change is its own commit so they can be reviewed (or reverted) independently. ## Test plan - [ ] CI green - [ ] Synthetic publish in staging for each package type (npm, pypi, nuget, oci, mcpb) — confirms validator changes don't regress - [ ] Quick browser check of `/` after deploy to confirm UI still loads with the new CSP - [ ] `curl https://<host>//evil.com/` — confirm the open redirect is closed 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f7663fe91a
commit
1201cbd82b
1 changed files with 330 additions and 48 deletions
|
|
@ -97,6 +97,7 @@ func WithSkipPaths(paths ...string) MiddlewareOption {
|
|||
// handle404 returns a helpful 404 error with suggestions for common mistakes
|
||||
func handle404(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/problem+json")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
|
||||
path := r.URL.Path
|
||||
|
|
@ -187,8 +188,28 @@ func NewHumaAPI(cfg *config.Config, registry service.RegistryService, mux *http.
|
|||
// Add UI and 404 handler for all other routes
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/" {
|
||||
// Serve UI at root
|
||||
// Serve UI at root. The page renders publisher-controlled content
|
||||
// (server names, descriptions, repository URLs) — server-side
|
||||
// validation plus a JS escape function are the primary XSS
|
||||
// defences; these headers are defence-in-depth in case any of
|
||||
// those slip.
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
// connect-src is unrestricted because the UI exposes a base-URL
|
||||
// selector (prod / staging / custom) that issues cross-origin
|
||||
// XHRs to whichever target the operator picks. Constraining
|
||||
// connect-src would silently break that affordance. The other
|
||||
// directives still meaningfully limit the page's attack surface.
|
||||
w.Header().Set("Content-Security-Policy",
|
||||
"default-src 'self'; "+
|
||||
"script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com; "+
|
||||
"style-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com; "+
|
||||
"img-src 'self' data:; "+
|
||||
"connect-src *; "+
|
||||
"frame-ancestors 'none'; "+
|
||||
"base-uri 'self'; "+
|
||||
"form-action 'self'")
|
||||
_, err := w.Write([]byte(v0.GetUIHTML()))
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to write response", http.StatusInternalServerError)
|
||||
|
|
|
|||
Loading…
Reference in a new issue