Skip to content
winfunc
Disclosure record · Mattermost

Zip bomb memory exhaustion in recursive document extraction (CVE-2026-3114)

Archive extraction limited compressed upload size but not decompressed entry size

MattermostCVE-2026-3114DisclosedSource record

CVSS 3.1 base score

Medium
6.5/ 10

Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Record

Project
Mattermost
Severity
Medium6.5
CVE
CVE-2026-3114
Disclosed
Trace
4 steps

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

  1. Source · attacker-controlledserver/channels/app/file.go:1625

    App.ExtractContentFromFileInfo

  2. Step 01server/channels/app/upload.go:349-352

    When file content extraction is enabled, uploaded file metadata can trigger ExtractContentFromFileInfo immediately 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)
    }
    
  3. Step 02server/channels/app/file.go:1636-1638

    The patched app layer now passes both ArchiveRecursion and the configured MaxFileSize into 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,
    })
    
  4. 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 MaxFileSize value 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)
    }
    
  5. 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)
    
  6. 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. 1

    Create a compressed archive whose on-disk size is within FileSettings.MaxFileSize but whose entry expands far beyond that size.

  2. 2

    Upload the archive to Mattermost.

  3. 3

    Content extraction opens the archive and walks decompressed entries.

  4. 4

    The vulnerable code calls io.ReadAll on an archive entry without a decompressed-size limit, allocating memory proportional to the expanded entry size.

Proof of concept

Reproduction5 stages
  1. 01

    Environment setup

    Use a vulnerable build before PR #35200 with FileSettings.ExtractContent enabled and FileSettings.ArchiveRecursion enabled. Keep FileSettings.MaxFileSize at a normal value such as 100 MB.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

Before and after−6+11
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)
View original source record

Check the upstream record for the project's current remediation status.

Your codebase

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.