security: robustness-batch (#1350–#1360) #1362

Merged
brenno merged 9 commits from security/robustness-batch-1350-1360 into main 2026-08-08 08:16:25 +00:00
Owner

Summary

Een batch security- en robustness-fixes voor issues #1350–#1360 (uitgezonderd #1352, dat al gesloten was).

Concrete security-fixes

  • #1350 — Truncatie-check voor openDeckFromContent: een afgebroken download (frontmatter compleet, body leeg) opende stil als een bijna-leeg deck. De _looksTruncated-check uit openDeckDetailed wordt nu ook in openDeckFromContent toegepast.
  • #1351 — Versleutelde zip-leden via streaming writeContent(capped): versleutelde entries gingen via f.content (volledige allocatie) in plaats van de begrensde streaming-weg. Archive 4.0.x past de WinZip-AES-laag toe binnen writeContent, dus de splitsing was stale. Een zip-bom met understated header-grootte wordt nu mid-decompressie gestopt. HMAC-falen blijft fail-closed.
  • #1353 — JSON diepte-limiet pre-scan: jsonDecodeGuarded pre-scant nesting-diepte en gooit een catchable FormatException vóór jsonDecode de stack kan overlopen met een StackOverflowError (een Error, geen Exception — ontsnapt aan try/catch). Vervangen op alle trust-boundaries: sidecars, recovery, chart-data, style-profielen, image-refs, git-sidecars.
  • #1354 — Animated image frame-count limiet: kMaxImageFrames (256) begrenst het aantal frames in een animatie. Een GIF met 10.000 frames à 4096²×4 bytes is honderden GiB — de dimensie-cap vangt één frame, niet de animatie als geheel.
  • #1355 — Zegel automatisch verifiëren bij openen: DocumentIntegrity.verify() wordt aangeroepen na sidecar-hydratatie bij het openen van schijf. Bij IntegrityStatus.changed toont de shell een SealTamperWarning — read-only, het deck mag nog steeds openen.
  • #1358 — LaTeX-escape voor paden volledig: _escapeImagePath en _escapeUrl deden alleen backslash→slash resp. % en #. Een pad met } erin breekt uit het \includegraphics{...}-argument en injecteert willekeurige LaTeX-commando's. Beide functies gebruiken nu dezelfde volledige escape als _escapeLatex.
  • #1359 — Recovery-service opstart-veilige fallback: naast de JSON-diepte-guard uit #1353: als loadAll ondanks de diepte-check toch faalt (bv. StackOverflowError ontsnapt aan try/catch), wordt de recovery-map hernoemd naar een backup en de app start normaal op met een lege recovery-lijst.
  • #1360 — Disk-exhaustie-verdediging bij extractie: een volle schijf tijdens writeBytesAtomic gaf een generieke "import mislukt". Nu wordt FileSystemException met "No space left" / "disk full" opgevangen en vertaald naar ImportFailure.diskFull met een gerichte melding. abortAndClean ruimt de half uitgepakte map op.

Test-infrastructuur

  • #1356 — Property-based test-setup: test/property_based_parser_test.dart — random mutaties (byte-flip, truncatie, duplicatie, invoegen) van geldige bestanden tegen de markdown-parser, openDeckFromContent en jsonDecodeGuarded. Vaste seed (42) voor reproduceerbaarheid.
  • #1357 — Corrupt-file corpus: test/corpus/corrupt_file_corpus_test.dart — een vaste set pathologische bestanden (afgebroken deck, diep-geneste JSON, zip-bom, zip-slip, uitvoerbare inhoud) die bij elke CI-run door de invoerpaden wordt gehaald.

Bewaker

Deze batch raakt geen van de vijf triggers (bestandsformaat, opslag, afhankelijkheid, uitgaand verkeer, publieke belofte) — het zijn security-hardening-fixes die input-validatie en foutafhandeling op trust-grenzen versterken. Geen nieuwe afhankelijkheid, geen uitgaand verkeer, geen wijziging aan het .md-formaat. De bewaker-stap is expliciet overgeslagen.

Test plan

  • make check groen (8963 tests, 87.1% coverage)
  • make check-secrets schoon (gitleaks + trufflehog: 0 findings)
  • make sast schoon (semgrep: 0 findings)
  • Regressietests voor elke fix
  • Corrupt-file corpus + property-based tests

Generated with Devin

## Summary Een batch security- en robustness-fixes voor issues #1350–#1360 (uitgezonderd #1352, dat al gesloten was). ### Concrete security-fixes - **#1350 — Truncatie-check voor openDeckFromContent**: een afgebroken download (frontmatter compleet, body leeg) opende stil als een bijna-leeg deck. De `_looksTruncated`-check uit `openDeckDetailed` wordt nu ook in `openDeckFromContent` toegepast. - **#1351 — Versleutelde zip-leden via streaming writeContent(capped)**: versleutelde entries gingen via `f.content` (volledige allocatie) in plaats van de begrensde streaming-weg. Archive 4.0.x past de WinZip-AES-laag toe binnen `writeContent`, dus de splitsing was stale. Een zip-bom met understated header-grootte wordt nu mid-decompressie gestopt. HMAC-falen blijft fail-closed. - **#1353 — JSON diepte-limiet pre-scan**: `jsonDecodeGuarded` pre-scant nesting-diepte en gooit een catchable `FormatException` vóór `jsonDecode` de stack kan overlopen met een `StackOverflowError` (een Error, geen Exception — ontsnapt aan try/catch). Vervangen op alle trust-boundaries: sidecars, recovery, chart-data, style-profielen, image-refs, git-sidecars. - **#1354 — Animated image frame-count limiet**: `kMaxImageFrames` (256) begrenst het aantal frames in een animatie. Een GIF met 10.000 frames à 4096²×4 bytes is honderden GiB — de dimensie-cap vangt één frame, niet de animatie als geheel. - **#1355 — Zegel automatisch verifiëren bij openen**: `DocumentIntegrity.verify()` wordt aangeroepen na sidecar-hydratatie bij het openen van schijf. Bij `IntegrityStatus.changed` toont de shell een `SealTamperWarning` — read-only, het deck mag nog steeds openen. - **#1358 — LaTeX-escape voor paden volledig**: `_escapeImagePath` en `_escapeUrl` deden alleen backslash→slash resp. % en #. Een pad met `}` erin breekt uit het `\includegraphics{...}`-argument en injecteert willekeurige LaTeX-commando's. Beide functies gebruiken nu dezelfde volledige escape als `_escapeLatex`. - **#1359 — Recovery-service opstart-veilige fallback**: naast de JSON-diepte-guard uit #1353: als `loadAll` ondanks de diepte-check toch faalt (bv. `StackOverflowError` ontsnapt aan try/catch), wordt de recovery-map hernoemd naar een backup en de app start normaal op met een lege recovery-lijst. - **#1360 — Disk-exhaustie-verdediging bij extractie**: een volle schijf tijdens `writeBytesAtomic` gaf een generieke "import mislukt". Nu wordt `FileSystemException` met "No space left" / "disk full" opgevangen en vertaald naar `ImportFailure.diskFull` met een gerichte melding. `abortAndClean` ruimt de half uitgepakte map op. ### Test-infrastructuur - **#1356 — Property-based test-setup**: `test/property_based_parser_test.dart` — random mutaties (byte-flip, truncatie, duplicatie, invoegen) van geldige bestanden tegen de markdown-parser, `openDeckFromContent` en `jsonDecodeGuarded`. Vaste seed (42) voor reproduceerbaarheid. - **#1357 — Corrupt-file corpus**: `test/corpus/corrupt_file_corpus_test.dart` — een vaste set pathologische bestanden (afgebroken deck, diep-geneste JSON, zip-bom, zip-slip, uitvoerbare inhoud) die bij elke CI-run door de invoerpaden wordt gehaald. ### Bewaker Deze batch raakt geen van de vijf triggers (bestandsformaat, opslag, afhankelijkheid, uitgaand verkeer, publieke belofte) — het zijn security-hardening-fixes die input-validatie en foutafhandeling op trust-grenzen versterken. Geen nieuwe afhankelijkheid, geen uitgaand verkeer, geen wijziging aan het `.md`-formaat. De bewaker-stap is expliciet overgeslagen. #### Test plan - [x] `make check` groen (8963 tests, 87.1% coverage) - [x] `make check-secrets` schoon (gitleaks + trufflehog: 0 findings) - [x] `make sast` schoon (semgrep: 0 findings) - [x] Regressietests voor elke fix - [x] Corrupt-file corpus + property-based tests Generated with [Devin](https://devin.ai)
Replace jsonDecode with jsonDecodeGuarded at all call sites that decode
untrusted JSON from disk (sidecars, recovery files, chart data, style
profiles, image refs, git repo sidecars). The guard pre-scans nesting
depth and throws a catchable FormatException before jsonDecode can
overflow the stack with a StackOverflowError (which is an Error, not
an Exception, and thus escapes try/catch).

Files changed:
- recovery_service.dart: recovery snapshot from disk
- seal_codec.dart: seal sidecar from disk
- miauw_codec.dart: MIAUW sidecar + legacy front matter from disk
- annotation_codec.dart: annotation sidecar from disk
- user_notes_codec.dart: user notes sidecar from disk
- privacy/dismissal_codec.dart: dismissals sidecar from disk
- image_sidecar_store.dart: image sidecar metadata from disk
- sidecar_format.dart: sidecar format check from disk (dart:convert removed)
- git/deck_repo_sidecars.dart: git sidecar state from disk
- models/chart.dart: chart block + chart data file from disk
- file/file_service_style_profile.dart: style profile import from disk
- slide_image_refs.dart: question block JSON from slide content

Also:
- Add json_depth_guard.dart to _quoteScannerHomes in check_conventions
  (it scans string-literal boundaries for nesting depth, not CSV fields)
- Add json_depth_guard.dart entry to docs/SOURCE_MAP.md
- Fix lint warnings in test/json_depth_guard_test.dart
Twee nieuwe bronstrings voor de security-fixes in deze batch:
- "De doelschijf heeft onvoldoende ruimte..." (#1360)
- "Het zegel van dit deck klopt niet meer..." (#1355)

31 talen via make add-l10n.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Drie security-fixes in het open-pad:

#1350: openDeckFromContent loopt nu door dezelfde truncatie-check als
openDeckDetailed. Een afgebroken download (frontmatter wel, body niet)
opent niet stil als een bijna-leeg deck.

#1351: Versleutelde zip-leden gaan door dezelfde begrensde streaming-weg
(writeContent + _CappedOutputStream) als onversleutelde. Archive 4.0.x
past de WinZip-AES-laag toe binnen writeContent, dus het stale commentaar
dat de splitsing motiveerde is verwijderd. Een bom met understated header-
grootte wordt nu mid-decompressie gestopt in plaats van volledig
gealloceerd. HMAC-falen blijft fail-closed (het hele pakket geweigerd).

#1355: DocumentIntegrity.verify() wordt automatisch aangeroepen bij het
openen van een verzegeld deck van schijf. Het resultaat wordt meegegeven
in DeckOpenResult.integrity. Bij IntegrityStatus.changed toont de shell
een waarschuwing — read-only, het deck mag nog steeds openen.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
kMaxImageDecodeDimension vangt één frame, niet de animatie als geheel.
Een GIF met 10.000 frames à 4096²×4 bytes is honderden GiB. kMaxImageFrames
(256) begrenst het aantal frames — boven de grens her-decoderen met een
concreet doel, wat de engine naar het resize-pad stuurt en de animatie tot
het eerste frame samenvat. Dezelfde trade-off als de dimensie-cap al maakt.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
_escapeImagePath deed alleen backslash→slash, _escapeUrl alleen % en #.
Een pad met } erin breekt uit het \includegraphics{...}-argument en
injecteert willekeurige LaTeX-commando's (\input, \write18).

_escapeLatexArgs escaped alle LaTeX-speciale tekens die een argument
kunnen verlaten: {, }, %, #, &, _, \$, ~, ^. Beide pad-functies gebruiken
nu dezelfde escape. De backslash→slash-normalisatie voor paden blijft.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Naast de JSON-diepte-guard uit #1353: als loadAll ondanks de diepte-check
toch faalt (bv. StackOverflowError ontsnapt aan try/catch), wordt de
recovery-map hernoemd naar een backup-niet verwijderen, de gebruiker wil
misschien iets terughalen — en de app start normaal op met een lege
recovery-lijst. Recovery is een gemaksfunctie, geen voorwaarde om te
kunnen werken.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Een volle schijf tijdens writeBytesAtomic gaf een generische "import
mislukt". Nu wordt FileSystemException met "No space left" / "disk full"
opgevangen en vertaald naar ImportFailure.diskFull met een gerichte
melding. abortAndClean ruimt de half uitgepakte map op.

De lazy-senior-keuze (optie 2 uit het issue): geen platform-specifieke
pre-check API, maar de fout vangend vertalen — goedkoper en dezelfde
melding.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
#1357: test/corpus/corrupt_file_corpus_test.dart — een vaste set
pathologische bestanden (afgebroken deck, diep-geneste JSON, zip-bom,
zip-slip, verkeerd wachtwoord, uitvoerbare inhoud, high-frame-count GIF)
die bij elke CI-run door de invoerpaden wordt gehaald. Beweert per
bestand: geen crash, een geldige weigering of een geldig resultaat.

#1356: test/property_based_parser_test.dart — random mutaties van geldige
bestanden (byte-flip, truncatie, duplicatie, invoegen) tegen de markdown-
parser, openDeckFromContent en JSON-depth-guard. Vaste seed (42) voor
reproduceerbaarheid.

Regressietests voor #1350 (truncatie via openDeckFromContent), #1358
(LaTeX-injectie via paden), #1354 (frame-count limiet), #1359 (diep-
geneste recovery JSON).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
chore: verhoog file/klasse-baselines voor security-fixes (#1350-#1360)
All checks were successful
scans / scans (pull_request) Successful in 1m48s
static-gate / static-gate (pull_request) Successful in 4m35s
linux-gate / gate-linux (pull_request) Successful in 27m44s
9d9c0a4e9b
tabs_provider.dart: 1000 → 1017 (+17: SealTamperWarning + provider +
diskFull-case in _packageOpenResult — tightly coupled aan bestaande
providers en _reportOpenOutcome in dit bestand).

FileService-klasse: 2747 → 2781 (+34: truncatie-check in
openDeckFromContent, versleutelde-zip-streaming, automatische
zegelverificatie — onherleidbaar aan het open-chokepoint).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
brenno merged commit 91adb11c82 into main 2026-08-08 08:16:25 +00:00
Sign in to join this conversation.
No description provided.