Cap archive extraction; stream zip/7z to tempfile (H-5, H-6) #108
No reviewers
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
qwc-open/asiakirjat!108
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/archive-extraction-limits"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Workstream item #13. Audit findings H-5 (decompression bomb / disk DoS) and H-6 (memory amplification from buffering archives in RAM).
H-5 — extraction limits
Each archive entry was capped at
maxFileSize(100 MB) but there was no aggregate cap and no entry-count cap. A 100 MB upload of bz2/xz could expand to multi-GB on disk; any uploader could fill the data volume.New per-extraction quotas, threaded through all formats:
maxTotalSize = 1 GBcumulative extracted bytesmaxEntries = 10000entriesextractStatscounter shared across all entrieslimitedWriterenforces the byte cap incrementally so a bomb file aborts as soon as the cumulative limit is crossed — not after GB are already on diskH-6 — buffering
extractZipandextract7zbothio.ReadAll-ed the whole archive into RAM (capped atmaxFileSize*10= 1 GB; in practice 100 MB due tohttp.MaxBytesReader). Concurrent uploads scaled memory linearly.Replace with
bufferArchiveToTempFile: copy the bounded input into a temp file viaio.LimitReader, pass the*os.File(which is anio.ReaderAt) plus the size tozip.NewReader/sevenzip.NewReader.closeAndRemovedefers cleanup at the call sites.Changes
internal/docs/archive.go— new constants,extractStats,limitedWriter,bufferArchiveToTempFile,closeAndRemove.extractZip,extract7z,extractTarall use them. Net change: cleaner control flow, +stats threading.internal/docs/archive_limits_test.go— three regression tests:maxEntries+11-byte files → rejected with "too many entries"Test plan
go test ./...passes🤖 Generated with Claude Code
Audit findings H-5 (decompression bomb / disk DoS) and H-6 (memory amplification from buffering whole archives in RAM). H-5 — extraction limits. Each archive entry was capped at maxFileSize (100 MB) but there was no aggregate cap and no entry-count cap. A 100 MB upload of bz2/xz could expand to multi-GB on disk, and any uploader could fill the volume. Add to each extraction pass: - maxTotalSize = 1 GB cumulative extracted bytes - maxEntries = 10000 entries - extractStats counter shared across all entries - limitedWriter enforces the byte cap incrementally so a bomb file aborts as soon as the cumulative limit is crossed, not after writing GB to disk. Wired through extractZip, extract7z, and extractTar (which covers .tar.gz/.tar.bz2/.tar.xz). H-6 — buffering. extractZip and extract7z both io.ReadAll-ed the whole archive into RAM (capped at maxFileSize*10 = 1 GB; in practice 100 MB due to http.MaxBytesReader). Concurrent uploads scaled memory linearly. Replace with bufferArchiveToTempFile: copy the bounded input into a temp file via io.LimitReader, then pass the *os.File to zip.NewReader or sevenzip.NewReader (both accept io.ReaderAt + size). closeAndRemove defers cleanup on the call sites. Three regression tests in archive_limits_test.go: - too-many-entries (maxEntries+1 tiny files → rejected) - total-byte bomb (11×100MB → rejected mid-stream, well before writing 1.1 GB to disk) - normal small zip still extracts cleanly Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>