GHSA-mx64-mj3q-7prj iskorotkov/avro: Denial-of-Service Vulnerability in Decoder
Memory Exhaustion via Unbounded Map Allocations in Avro DecoderSummaryThe Avro map decoder accepted attacker-controlled block-element counts from the wire format and grew the destination map without enforcing an upper bound. The slice decoder already had `Config.MaxSliceAllocSize` for the equivalent attack against arrays; the map decoder had no analogous limit, so a producer could declare an arbitrarily large map (in one block, or chunked across many sub-limit blocks) and exhaust process memory until the OOM killer fired.The fix introduces `Config.MaxMapAllocSize` with cumulative enforcement across block boundaries. **The new limit is opt-in**: the field defaults to zero, which preserves the previous unbounded behavior for backward compatibility. **Upgrading to `v2.33.0` alone does not mitigate the issue** — consumers of untrusted Avro data must explicitly set `MaxMapAllocSize` on their `avro.Config`.DescriptionAvro maps are encoded as a sequence of blocks; each block declares a `long` element count followed by that many key/value pairs. The decoder uses these counts both to size the destination map and as the loop bound for reading entries.Pre-fix, the map decoder enforced no upper limit at any layer:No per-block element-count check.No cumulative across-block element-count check.No memory-budget check before `make(map[...]..., n)` or before growing the map.The slice decoder had been hardened via `Config.MaxSliceAllocSize` and tracked cumulatively across blocks; the map decoder was a missing-by-symmetry gap. Even a partial per-block bound on maps would have been insufficient on its own — Avro permits encoding a logical map as many small blocks, so a producer could split a 10 GB map into 10,000 sub-MaxMapAllocSize blocks and still drive total allocation past any single-block threshold. The fix tracks cumulative entry count at block-header boundaries — *before* the block's entries are decoded into the map — and errors out before allocation when the running total would exceed the configured cap.Two decoder variants were affected, both in `codec_map.go`:`mapDecoder.Decode` — string-keyed maps.`mapDecoderUnmarshaler.Decode` — `encoding.TextUnmarshaler`-keyed maps (e.g. `map[CustomKey]V` where `*CustomKey` implements `UnmarshalText`).Affected components| File | Symbol | Pre-fix behavior | Post-fix behavior |
|------|--------|------------------|-------------------|
| `config.go` | `Config.MaxMapAllocSize` | Field did not exist | New `int` field; default zero means unlimited (back-compat) |
| `codec_map.go` | `mapDecoder.Decode` | Read block count, grew map unbounded | Validates cumulative count against `MaxMapAllocSize` at each block header |
| `codec_map.go` | `mapDecoderUnmarshaler.Decode` | Same | Same |PR [#5](https://github.com/iskorotkov/avro/pull/5) (`fix/map-alloc-chunking-bypass`) covers both decoders and adds chunking-attack tests for both. The same PR also adds the previously-missing chunking-attack test coverage for the slice path in `534c7518` — the slice *logic* was already correct, only its test coverage was incomplete.Technical detailsThe fix mirrors the slice decoder's pattern:At each block header, read the element count as `int64`.Add it to a running total maintained across the block loop.If the running total exceeds `Config.MaxMapAllocSize` (when nonzero), return an error before allocating any of that block's entries.Otherwise, decode the block's entries into the map.Per-block enforcement alone would be bypassable by chunking; cumulative tracking closes that. The check sits at the block-header read, *before* per-entry allocation, so a single oversized block also cannot allocate first and then fail post-hoc.`Config.MaxMapAllocSize` semantics match `Config.MaxSliceAllocSize`: zero means unlimited, any positive value is the cumulative cap on element count (not byte size).Fixed behavior`v2.33.0` adds the `MaxMapAllocSize` configuration field and the cumulative-enforcement logic in both map decoders. Both decoders return a descriptive error when the cumulative entry count would exceed the configured cap; no entries are allocated past the limit.Tests added in PR #5 cover, for both `mapDecoder` and `mapDecoderUnmarshaler`:Single-block allocation exceeding the limit (rejected before allocation).Chunking attack: multiple sub-limit blocks whose cumulative count exceeds the limit (rejected at the block-header that crosses the threshold).Multi-block under the limit (decoded normally).Affected versions`github.com/hamba/avro/v2` — all versions up to and including `v2.31.0` (repository is read-only upstream).`github.com/iskorotkov/avro/v2` — all versions prior to `v2.33.0`. Note: `v2.33.0` and later are vulnerable *by default* and only protected when `MaxMapAllocSize` is explicitly configured — see Mitigation.Fixed versions`github.com/iskorotkov/avro/v2` `v2.33.0` and later, **with `Config.MaxMapAllocSize` explicitly set to a non-zero value**.A bare upgrade to `v2.33.0` without setting `MaxMapAllocSize` leaves the decoder in the same unbounded state as `v2.32.0`. This is a backward-compatibility choice; a future major version may flip the default. Until then, treat this advisory as requiring both an upgrade *and* a configuration change.There is no upstream fix for `github.com/hamba/avro/v2` — module path is archived. Migrate to the fork as described under Mitigation.MitigationMigrate from `github.com/hamba/avro/v2` to `github.com/iskorotkov/avro/v2 >= v2.33.0` **and** configure an allocation cap appropriate for your schema. The recommended approach for processes that decode untrusted input is a dedicated frozen config, used at every relevant call site, rather than mutating `avro.DefaultConfig`:Choose the values based on the largest legitimate map your schema produces; a value 2–10× that ceiling provides headroom for benign variance while still bounding worst-case memory.For consumers that prefer the original import path, a `replace` directive in `go.mod` is supported:`replace` is honoured only for the **main** module of a build — transitive consumers must add their own `replace`, or migrate the import path directly.If you cannot upgrade immediately, the only structural workarounds are out-of-band: run decoders in memory-constrained child processes or cgroups so an OOM is contained, reject inputs from sources without resource controls, and apply per-request decode deadlines so a runaway decode at least times out before the OOM killer fires.Proof-of-concept inputTwo attack shapes, both targeting `map[string]int`:**Single-block, oversize block count.** Emit one block header declaring `n = 2³¹ − 1` (or any value whose `n × averageEntrySize` exceeds available memory) followed by truncated entries. Pre-fix, the decoder pre-allocates `make(map[string]int, n)`, which fails or stalls long before EOF is reached.**Chunking bypass.** Emit `k` blocks each declaring `n / k` elements, with `n / k` below any plausible per-block threshold but `n` itself well into the GB range. Pre-fix, the decoder happily grows the map block-by-block until the OS kills the process. Post-fix with `MaxMapAllocSize = 10_000`, the decoder rejects whichever block-header read pushes cumulative count past 10,000.Either shape can be produced by hand-crafting the wire bytes; no `iskorotkov/avro` writer is needed to generate them.ReferencesFix PR: [iskorotkov/avro#5](https://github.com/iskorotkov/avro/pull/5)Fix commit: [`5192df9`](https://github.com/iskorotkov/avro/commit/5192df96a158999344ac96ebcb1f7461d626f6d7) (`codec_map.go`, `config.go`, tests)Slice-path chunking-attack test coverage added in the same PR: [`534c7518`](https://github.com/iskorotkov/avro/commit/534c7518152a893d8b4dea962669bd1123308a00)Release: [`v2.33.0`](https://github.com/iskorotkov/avro/releases/tag/v2.33.0)Security policy: [`SECURITY.md`](https://github.com/iskorotkov/avro/blob/main/SECURITY.md)Related advisories on this fork: [`GHSA-mc57-h6j3-3hmv`](https://github.com/iskorotkov/avro/security/advisories/GHSA-mc57-h6j3-3hmv) (integer overflow), [`GHSA-w8j3-pq8g-8m7w`](https://github.com/iskorotkov/avro/security/advisories/GHSA-w8j3-pq8g-8m7w) (CPU exhaustion — the same chunked-payload shape may trigger both before allocation pressure kicks in)Cross-module precedent on `hamba/avro`: [`GO-2023-1930`](https://pkg.go.dev/vuln/GO-2023-1930) / `CVE-2023-37475` / `GHSA-9x44-9pgq-cf45`Upstream (read-only): [`hamba/avro`](https://github.com/hamba/avro)Credits**Fix author** (commit `5192df9`, PR #5 — `MaxMapAllocSize` config field, cumulative enforcement in both map decoders, chunking-attack tests for slices and maps): Ivan Korotkov ([@iskorotkov](https://github.com/iskorotkov))**Review** (commit `a5fbddcb`, "address review comments"): Daniel Błażewicz ([@klajok](https://github.com/klajok))Timeline**2026-04-30** — `MaxMapAllocSize` introduced (`5192df9`); chunking-attack test coverage for slices added (`534c7518`).**2026-05-01** — PR #5 merged into `main`.**2026-05-06** — `v2.33.0` tagged and released.**2026-05-07** — Advisory published.**2026-05-15** — Advisory revised.
CVSS v4.0 score: 8.7
CVSS v3.1 score: 7.5