JSON stack-overflow bij diep geneste input (jsonDecode is recursief, geen diepte-limiet) #1353

Closed
opened 2026-08-07 22:17:27 +00:00 by brenno · 0 comments
Owner

Bevinding

Dart's jsonDecode is recursief geïmplementeerd. Een sidecar van 16 MiB of
grafiekdata van 8 MiB vol [[[[[[...]]]]]] laat de call-stack overlopen en
crasht de app — ondanks de byte-cap die er wél is. De byte-cap vangt grootte,
niet diepte.

Er zijn 77 jsonDecode-call sites in de codebase, waarvan er ~20 onvertrouwde
input decoderen: sidecars (.ink.json, .user-notes.json, .miauw.json,
.seal.json, .dismissals.json), grafiekdata, OpenKat-import, collab-blobs,
recovery-bestanden, stijlprofielen. Elke daarvan is een pad waar een
kwaadwillend of corrupt bestand de app kan laten crashen.

De codebase begrijpt dit patroon al — op andere plekken

Diepte-limieten bestaan al voor drie andere invoerformaten:

  • LibrePlan XML: libreplanXmlMaxDepth
    (lib/services/libreplan/libreplan_xml.dart:283)
  • ASN1 DER: _maxDerDepth
    (lib/utils/asn1_der.dart:128)
  • Keynote proto: depth > 16
    (lib/services/import/importers/keynote/key_text_salvage.dart:61)

Maar niet voor JSON. Dat is de asymmetrie.

Oplossingsrichting

Twee opties, oplopend in moeite:

  1. Pre-scan: tel de nesting-diepte van de raw string vóór jsonDecode
    (tel ongebalanceerde [/{). Goedkoop, geen dependency, vangt het
    pathologische geval. Weiger boven een redelijke grens (bv. 256).

  2. Depth-guarded parser: een wrapper rond jsonDecode of een eigen
    decoder die een diepte-limiet handhaaft tijdens het parsen. Meer werk,
    maar vangt ook geneste structuren die een simpele pre-scan niet ziet
    (bv. {"a":{"b":{"c":...}}} met strings die [ bevatten).

Optie 1 is de lazy-senior-keuze: het pathologische geval is diep geneste
brackets, en die vang je met een lineaire scan. Optie 2 is pas nodig als er
een realistisch geneste-strings-aanval is, wat voor deze invoer onwaarschijnlijk
is.

Herkomst

Gevonden tijdens security research naar defense-in-depth voor OciDeck. De
bestaande byte-caps (sidecar 16 MiB, grafiekdata 8 MiB) vangen grootte, maar
niet diepte — en een stack-overflow is precies de faalmodus die grootte-caps
niet dekken.

## Bevinding Dart's `jsonDecode` is recursief geïmplementeerd. Een sidecar van 16 MiB of grafiekdata van 8 MiB vol `[[[[[[...]]]]]]` laat de call-stack overlopen en crasht de app — ondanks de byte-cap die er wél is. De byte-cap vangt grootte, niet diepte. Er zijn 77 `jsonDecode`-call sites in de codebase, waarvan er ~20 onvertrouwde input decoderen: sidecars (`.ink.json`, `.user-notes.json`, `.miauw.json`, `.seal.json`, `.dismissals.json`), grafiekdata, OpenKat-import, collab-blobs, recovery-bestanden, stijlprofielen. Elke daarvan is een pad waar een kwaadwillend of corrupt bestand de app kan laten crashen. ## De codebase begrijpt dit patroon al — op andere plekken Diepte-limieten bestaan al voor drie andere invoerformaten: - LibrePlan XML: `libreplanXmlMaxDepth` (`lib/services/libreplan/libreplan_xml.dart:283`) - ASN1 DER: `_maxDerDepth` (`lib/utils/asn1_der.dart:128`) - Keynote proto: `depth > 16` (`lib/services/import/importers/keynote/key_text_salvage.dart:61`) Maar niet voor JSON. Dat is de asymmetrie. ## Oplossingsrichting Twee opties, oplopend in moeite: 1. **Pre-scan**: tel de nesting-diepte van de raw string vóór `jsonDecode` (tel ongebalanceerde `[`/`{`). Goedkoop, geen dependency, vangt het pathologische geval. Weiger boven een redelijke grens (bv. 256). 2. **Depth-guarded parser**: een wrapper rond `jsonDecode` of een eigen decoder die een diepte-limiet handhaaft tijdens het parsen. Meer werk, maar vangt ook geneste structuren die een simpele pre-scan niet ziet (bv. `{"a":{"b":{"c":...}}}` met strings die `[` bevatten). Optie 1 is de lazy-senior-keuze: het pathologische geval is diep geneste brackets, en die vang je met een lineaire scan. Optie 2 is pas nodig als er een realistisch geneste-strings-aanval is, wat voor deze invoer onwaarschijnlijk is. ## Herkomst Gevonden tijdens security research naar defense-in-depth voor OciDeck. De bestaande byte-caps (sidecar 16 MiB, grafiekdata 8 MiB) vangen grootte, maar niet diepte — en een stack-overflow is precies de faalmodus die grootte-caps niet dekken.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
LibreKAT/Ocideck#1353
No description provided.