Versleutelde zip-leden alloceren vol vóór cap-check (stale commentaar blokkeert streaming-inflate) #1351

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

Bevinding

FileService.decodePackageEntries heeft twee takken voor het uitlezen van een
zip-lid:

  • Onversleuteld: f.writeContent(_CappedOutputStream(remaining)) — de
    inflater streamt incrementeel naar een begrensde output, en een deflate-bom
    met understated header-grootte wordt mid-decompressie gestopt
    (ExtractionLimitException). Dit is de sterke guard.
  • Versleuteld (WinZip-AES): f.content — decodeert de hele entry in één
    keer in het geheugen, en pas daarna wordt extracted + raw.length > maxBytes
    gecheckt. Een bom die zijn uncompressed size understated alloceert dus de
    volledige plaintext vóór de weigering.

De code motiveert die splitsing met:

"WinZip-AES wordt alleen door de content-getter ontsleuteld — de streaming
writeContent inflate-weg past de AES-laag niet toe en zou onleesbare bytes
opleveren."

Dat commentaar klopt niet voor de geïnstalleerde archive-versie

De repo draait op archive: 4.0.9. Daarin doet ZipFile.decompress(output)
(<ref_snippet file="/Users/brennodewinter/.pub-cache/hosted/pub.dev/archive-4.0.9/lib/src/codecs/zip/zip_file.dart" lines="164-193" />) expliciet wél de AES-laag vóór het inflaten:

  1. _rawContent = _decodeAes(_rawContent!) — ontsleutelt de gecomprimeerde
    payload (begrensd door de 512 MiB input-cap).
  2. ZLibDecoder().decodeStream(_rawContent!, output, raw: true) — streamt de
    inflate naar de meegegeven output.

En ArchiveFile.writeContent(output) roept decompress(output) aan
(<ref_snippet file="/Users/brennodewinter/.pub-cache/hosted/pub.dev/archive-4.0.9/lib/src/archive/archive_file.dart" lines="153-167" />). De streaming writeContent-weg past de AES-laag dus wél toe. Het commentaar is verouderd of is nooit juist geweest voor 4.0.x.

Oplossingsrichting

De twee takken samenvoegen: ook voor versleutelde leden writeContent(capped)
gebruiken, met dezelfde HMAC-failure-catch die er al is. De _decodeAes-throw
("macs don't match") gebeurt dan binnen writeContent en wordt op dezelfde
plek gevangen. De inflate wordt daarna mid-stream begrensd in plaats van volledig
gealloceerd.

Inschatting: ~10-15 regels diff. De f.content-allocatie van de hele
ongecomprimeerde entry verdwijnt, en daarmee het gat bij een bom met understated
header-grootte. _decodeAes alloceert nog steeds de volledige gecomprimeerde
plaintext (onvermijdelijk: WinZip-AES vereist de volledige content voor de
HMAC), maar dat is begrensd door de 512 MiB input-cap, niet door de
ongecomprimeerde grootte — een orde-grootte beter.

Caveat

Het commentaar bestond om een reden — mogelijk was het waar voor een oudere
archive-versie. Daarom: schrijf eerst een test die een versleutelde entry via
writeContent(capped) aanbiedt en bevestigt dat (a) de bytes kloppen en (b) een
bom mid-stream stopt. Pas dan de code aan. Zonder die test herhaal je de fout
die het commentaar maakte — aannemen in plaats van verifiëren.

Herkomst

Gevonden tijdens security research naar de robustness van OciDeck bij het
openen van corrupte bestanden en resource-uitputting. De bevinding uit de
eerdere ronde ("versleutelde leden hebben niet de streaming-inflate-cap") bleek
bij nader inzien niet te kloppen voor de geïnstalleerde archive-versie — de
package stelt de streaming AES-weg wél bloot, maar de OciDeck-code gebruikt hem
niet vanwege een stale commentaar.

## Bevinding `FileService.decodePackageEntries` heeft twee takken voor het uitlezen van een zip-lid: - **Onversleuteld**: `f.writeContent(_CappedOutputStream(remaining))` — de inflater streamt incrementeel naar een begrensde output, en een deflate-bom met understated header-grootte wordt mid-decompressie gestopt (`ExtractionLimitException`). Dit is de sterke guard. - **Versleuteld (WinZip-AES)**: `f.content` — decodeert de hele entry in één keer in het geheugen, en pas daarna wordt `extracted + raw.length > maxBytes` gecheckt. Een bom die zijn uncompressed size understated alloceert dus de volledige plaintext vóór de weigering. De code motiveert die splitsing met: > "WinZip-AES wordt alleen door de content-getter ontsleuteld — de streaming > writeContent inflate-weg past de AES-laag niet toe en zou onleesbare bytes > opleveren." ## Dat commentaar klopt niet voor de geïnstalleerde archive-versie De repo draait op `archive: 4.0.9`. Daarin doet `ZipFile.decompress(output)` (<ref_snippet file="/Users/brennodewinter/.pub-cache/hosted/pub.dev/archive-4.0.9/lib/src/codecs/zip/zip_file.dart" lines="164-193" />) expliciet wél de AES-laag vóór het inflaten: 1. `_rawContent = _decodeAes(_rawContent!)` — ontsleutelt de gecomprimeerde payload (begrensd door de 512 MiB input-cap). 2. `ZLibDecoder().decodeStream(_rawContent!, output, raw: true)` — streamt de inflate naar de meegegeven output. En `ArchiveFile.writeContent(output)` roept `decompress(output)` aan (<ref_snippet file="/Users/brennodewinter/.pub-cache/hosted/pub.dev/archive-4.0.9/lib/src/archive/archive_file.dart" lines="153-167" />). De streaming `writeContent`-weg past de AES-laag dus wél toe. Het commentaar is verouderd of is nooit juist geweest voor 4.0.x. ## Oplossingsrichting De twee takken samenvoegen: ook voor versleutelde leden `writeContent(capped)` gebruiken, met dezelfde HMAC-failure-catch die er al is. De `_decodeAes`-throw ("macs don't match") gebeurt dan binnen `writeContent` en wordt op dezelfde plek gevangen. De inflate wordt daarna mid-stream begrensd in plaats van volledig gealloceerd. Inschatting: ~10-15 regels diff. De `f.content`-allocatie van de hele ongecomprimeerde entry verdwijnt, en daarmee het gat bij een bom met understated header-grootte. `_decodeAes` alloceert nog steeds de volledige gecomprimeerde plaintext (onvermijdelijk: WinZip-AES vereist de volledige content voor de HMAC), maar dat is begrensd door de 512 MiB input-cap, niet door de ongecomprimeerde grootte — een orde-grootte beter. ## Caveat Het commentaar bestond om een reden — mogelijk was het waar voor een oudere archive-versie. Daarom: schrijf eerst een test die een versleutelde entry via `writeContent(capped)` aanbiedt en bevestigt dat (a) de bytes kloppen en (b) een bom mid-stream stopt. Pas dan de code aan. Zonder die test herhaal je de fout die het commentaar maakte — aannemen in plaats van verifiëren. ## Herkomst Gevonden tijdens security research naar de robustness van OciDeck bij het openen van corrupte bestanden en resource-uitputting. De bevinding uit de eerdere ronde ("versleutelde leden hebben niet de streaming-inflate-cap") bleek bij nader inzien niet te kloppen voor de geïnstalleerde archive-versie — de package stelt de streaming AES-weg wél bloot, maar de OciDeck-code gebruikt hem niet vanwege een stale commentaar.
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#1351
No description provided.