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
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -71,9 +72,11 @@ func TrailingSlashMiddleware(next http.Handler) http.Handler {
|
|||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Only redirect if the path is not "/" and ends with a "/"
|
||||
if r.URL.Path != "/" && strings.HasSuffix(r.URL.Path, "/") {
|
||||
// Create a copy of the URL and remove the trailing slash
|
||||
// path.Clean both removes the trailing slash and collapses any
|
||||
// leading "//" to "/", which prevents an open-redirect via a
|
||||
// protocol-relative path like "//evil.com/" (GHSA-v8vw-gw5j-w7m6).
|
||||
newURL := *r.URL
|
||||
newURL.Path = strings.TrimSuffix(r.URL.Path, "/")
|
||||
newURL.Path = path.Clean(r.URL.Path)
|
||||
|
||||
// Use 308 Permanent Redirect to preserve the request method
|
||||
http.Redirect(w, r, newURL.String(), http.StatusPermanentRedirect)
|
||||
|
|
@ -127,6 +130,17 @@ func NewServer(cfg *config.Config, registryService service.RegistryService, metr
|
|||
Addr: cfg.ServerAddress,
|
||||
Handler: handler,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
// WriteTimeout intentionally not set: the publish path runs
|
||||
// outbound package validators sequentially (npm/pypi/nuget up to
|
||||
// 10s each, OCI up to 30s), so any tight cap could cut off a
|
||||
// legitimate multi-package publish mid-response — surfacing as a
|
||||
// truncated read to the publisher even when the DB commit
|
||||
// succeeded. Slow-response-read DoS is bounded upstream by
|
||||
// NGINX ingress timeouts and the per-IP rate limit. Revisit once
|
||||
// validators are parallelised or per-request package counts are
|
||||
// bounded.
|
||||
IdleTimeout: 120 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue