The openapi3filter.ValidationHandler (the exported, documented request-validation middleware) hard-wires the legacy router (routers/legacy). That router matches the incoming request method verbatim against a trie built exclusively from upper-cased canonical HTTP methods. A request carrying a method token that is not one of the 9 standard methods — a legitimate-but-unsupported token such as PROPFIND, or a non-canonical case variant such as lowercase get — misses the trie, leaves route == nil, and for an existing static path falls through to PathItem.GetOperation(method), whose default: case calls panic(...). The panic fires inside route resolution, before any authentication runs. This is an unauthenticated, remotely triggerable denial of service (recovered per-connection panic + unbounded error-log growth).
openapi3filter/validation_handler.go:39 — ValidationHandler.Load() always constructs legacyrouter.NewRouter(doc); there is no alternative router option on this path.routers/legacy/router.go:109 — the request method is used verbatim (method, url := req.Method, ...); there is no canonicalization or rejection of unknown method tokens.routers/legacy/router.go:69-70 — the trie is populated with strings.ToUpper(method) keys taken from pathItem.Operations() (canonical http.Method* constants only).routers/legacy/router.go:141 — root.Match(method + " " + remainingPath) misses for a non-canonical/unsupported method → route == nil.routers/legacy/router.go:146-150 — in the route == nil block, doc.Paths.Value(path) returns non-nil for an existing static path, and control reaches pathItem.GetOperation(method).openapi3/path_item.go:174 — GetOperation's switch method has no matching case → default: panic(fmt.Errorf("unsupported HTTP method %q", method)).An unauthenticated attacker sends a single request with a non-standard HTTP method token to any existing static path. The handler goroutine panics; Go's net/http recovers the panic per connection (process survives), aborts the connection (client sees a reset / 500), and logs a full stack trace. Repeated requests cause connection churn and unbounded http: panic serving log growth, and the request never reaches authentication. Availability impact (A:L) under the default synchronous ValidationHandler middleware; on non-net/http hosts or when reached on an application goroutine the ceiling is a full crash.
Serve any spec with a static path (e.g. /ping) behind openapi3filter.ValidationHandler, then:
printf 'PROPFIND /ping HTTP/1.1\r\nHost: x\r\n\r\n' | nc target 8080 # connection aborted (panic)
printf 'get /ping HTTP/1.1\r\nHost: x\r\n\r\n' | nc target 8080 # connection aborted (panic)
printf 'GET /ping HTTP/1.1\r\nHost: x\r\n\r\n' | nc target 8080 # 200 OK (process survived)
Reproduced end-to-end against a real net/http.Server + ValidationHandler: PROPFIND /ping and lowercase get /ping both abort the connection with a panic at path_item.go:174 (stack: GetOperation <- legacy.FindRoute router.go:150 <- ValidationHandler.validateRequest validation_handler.go:84); the process stays alive and subsequent GET /ping returns 200.
PROPFIND /ping HTTP/1.1 (any known static path). Guard: net/http method validation. Bypass proof: net/http accepts any valid HTTP token as a method — PROPFIND and lowercase get both reach the handler.legacy/router.go:141 root.Match("PROPFIND /ping"). Guard: trie match. Bypass proof: trie holds only upper-cased canonical methods → miss → route == nil.route == nil block: router.go:146 doc.Paths.Value("/ping") → non-nil for an existing static path (exact map lookup). Guard: pathItem == nil check (l.147). Bypass proof: static path exists → not nil → proceeds.router.go:150 pathItem.GetOperation("PROPFIND") → path_item.go:174 default: panic(...).PROPFIND and lowercase get both reached the handler and panicked.FindRoute entry and GetOperation; req.Method is used verbatim (router.go:109).git show v0.144.0:openapi3/path_item.go shows panic(...) at line 174; git show v0.144.0:routers/legacy/router.go shows verbatim method use and the unguarded GetOperation call. git log v0.144.0..HEAD on these files is empty (no post-tag fix).The panic requires the request path to be an exact static (non-templated) key in the served spec. A concrete path against a /{id} template returns a clean 500 (not a panic), because Paths.Value is an exact map lookup. Static paths (/health, /login, /version, /metrics, /ping, /) are ubiquitous in real specs and discoverable from public specs/standard endpoints, so a vulnerable target almost always exists. This makes the finding arguably broader than existing panic advisories, as it needs no special spec construct — any static path suffices.
<= 0.144.0 (present on the latest release tag v0.144.0; long-standing; no patched version).
In the legacy router, canonicalize/validate the request method before route resolution and return ErrMethodNotAllowed/ErrPathNotFound for unknown methods, or make PathItem.GetOperation return an error (nil) for unrecognized methods instead of calling panic.
Reported by zx (Jace) — GitHub: @manus-use
{
"cwe_ids": [
"CWE-20",
"CWE-248"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-15T20:15:48Z",
"nvd_published_at": "2026-10-15T16:17:30Z",
"severity": "MODERATE"
}