Zip bomb memory exhaustion in recursive document extraction (CVE-2026-3114)
Archive extraction limited compressed upload size but not decompressed entry size
CVSS 3.1 base score
MediumVector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
Summary
Source
App.ExtractContentFromFileInfo
server/channels/app/file.go:1625
Sink
archiveExtractor.Extract
server/platform/services/docextractor/archive.go:100
Mattermost enforces FileSettings.MaxFileSize on uploaded files, but the document extraction service previously did not apply that same limit to each decompressed archive entry. When file-content extraction and archive recursion were enabled, archiveExtractor.Extract mounted an uploaded archive, walked entries with fs.WalkDir, opened each entry, and called io.ReadAll(file) before handing the decompressed bytes to sub-extractors. A small compressed archive could therefore expand into very large in-memory entry data and exhaust server memory during content extraction.
The fix threads MaxFileSize through docextractor.ExtractSettings and the Extractor interface, passes the configured file-size limit from App.ExtractContentFromFileInfo, and wraps archive entry readers with utils.NewLimitedReaderWithError before io.ReadAll. The original fix is PR #35200 / commit b947f1c38a675688c5fc9ade696d0f0f2bad430a; backports include #35220, #35279, and #35282.
Severity
- Attack vectorAV
- Network
- Attack complexityAC
- Low
- Privileges requiredPR
- Low
- User interactionUI
- None
- ScopeS
- Unchanged
- ConfidentialityC
- None
- IntegrityI
- None
- AvailabilityA
- High
Metric values as published in the disclosure vector. Meters show how far each value raises exposure.
Source-to-sink trace
- Source · attacker-controlledserver/
channels/ app/ file.go:1625 App.ExtractContentFromFileInfo
- Step 01server/
channels/ app/ upload.go:349-352 When file content extraction is enabled, uploaded file metadata can trigger
ExtractContentFromFileInfoimmediately after storage. The extract-content job can also process stored files later.go if *a.Config().FileSettings.ExtractContent { infoCopy := info err := a.ExtractContentFromFileInfo(rctx, &infoCopy) } - Step 02server/
channels/ app/ file.go:1636-1638 The patched app layer now passes both
ArchiveRecursionand the configuredMaxFileSizeinto the document extractor.go text, err := docextractor.Extract(rctx.Logger(), fileInfo.Name, file, docextractor.ExtractSettings{ ArchiveRecursion: *a.Config().FileSettings.ArchiveRecursion, MaxFileSize: *a.Config().FileSettings.MaxFileSize, }) - Step 03server/
platform/ services/ docextractor/ docextractor.go:36-48 When archive recursion is enabled, the archive extractor is configured with a sub-extractor chain and receives the
MaxFileSizevalue through the shared extractor interface.go if settings.ArchiveRecursion { enabledExtractors.Add(&archiveExtractor{SubExtractor: enabledExtractors}) } else { enabledExtractors.Add(&archiveExtractor{}) } if enabledExtractors.Match(filename) { return enabledExtractors.Extract(filename, r, settings.MaxFileSize) } - Step 04server/
platform/ services/ docextractor/ archive.go:87-105 Before the fix, each archive entry was read with an unbounded
io.ReadAll(file). The patched code wraps the entry in a limited reader before reading decompressed bytes into memory.go file, err := fsys.Open(path) if err != nil { return err } defer file.Close() var reader io.Reader = file if maxFileSize > 0 { reader = utils.NewLimitedReaderWithError(file, maxFileSize) } data, err := io.ReadAll(reader) if err != nil { return fmt.Errorf("error reading archive entry %s: %w", path, err) } subtext, extractErr := ae.SubExtractor.Extract(filename, bytes.NewReader(data), maxFileSize) - Sinkserver/
platform/ services/ docextractor/ archive.go:100 archiveExtractor.Extract
Impact
Reported impact
A small authenticated upload can cause disproportionate server memory allocation during content extraction. Repeated uploads or a sufficiently large decompressed entry can exhaust memory, destabilize extraction workers, and degrade or crash the Mattermost process.
Attack surface
Mattermost servers with file uploads enabled and file-content extraction configured. The decompressed-entry risk specifically depends on recursive archive extraction (FileSettings.ArchiveRecursion) because sub-extraction reads archive entries into memory.
Preconditions
The attacker needs an account or integration capable of uploading files to a channel where the server will process the file for text extraction. The compressed archive must fit within the configured upload size limit while expanding to very large entry data.
Attack path
- 1
Create a compressed archive whose on-disk size is within
FileSettings.MaxFileSizebut whose entry expands far beyond that size. - 2
Upload the archive to Mattermost.
- 3
Content extraction opens the archive and walks decompressed entries.
- 4
The vulnerable code calls
io.ReadAllon an archive entry without a decompressed-size limit, allocating memory proportional to the expanded entry size.
Proof of concept
- 01
Environment setup
Use a vulnerable build before PR #35200 with
FileSettings.ExtractContentenabled andFileSettings.ArchiveRecursionenabled. KeepFileSettings.MaxFileSizeat a normal value such as 100 MB. - 02
Target configuration
The vulnerable path is the document extraction service, not the upload size check. The archive itself must be accepted by upload limits while containing an entry that decompresses to a much larger size.
- 03
Exploit delivery
Upload a crafted ZIP archive with a highly compressed large text entry. The server stores the uploaded file and invokes extraction either immediately or through the extract-content job.
- 04
Expected response
On vulnerable builds, extraction attempts to allocate the full decompressed entry via
io.ReadAll(file). On fixed builds, reading the entry fails once the configured max file size is exceeded. - 05
Outcome
The patch turns decompressed-entry overrun into a bounded extraction error instead of unbounded memory growth.
Remediation
Guidance
Propagate the configured file-size limit into the archive extraction layer and apply it to decompressed entries before reading them into memory. Upload-size checks are not enough because compressed size and decompressed entry size are different trust boundaries.
data, err := io.ReadAll(file)if err != nil {return err}subtext, extractErr := ae.SubExtractor.Extract(filename, bytes.NewReader(data))var reader io.Reader = fileif maxFileSize > 0 { reader = utils.NewLimitedReaderWithError(file, maxFileSize)} data, err := io.ReadAll(reader)if err != nil { return fmt.Errorf("error reading archive entry %s: %w", path, err)} subtext, extractErr := ae.SubExtractor.Extract(filename, bytes.NewReader(data), maxFileSize)
Check the upstream record for the project's current remediation status.
Investigate the paths that matter in your codebase.
Winfunc can trace relevant code paths, preserve supporting evidence, and prepare remediation suggestions for engineering review within an agreed scope.
