Skip to content
winfunc
Disclosure record · Mattermost

mmctl export downloads created world-readable local files (CVE-2026-3113)

Export files downloaded with mmctl inherited unsafe filesystem permissions

MattermostCVE-2026-3113DisclosedSource record

Severity rating

Medium
Medium

No numeric base score was published with this disclosure.

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

Record

Project
Mattermost
Severity
Medium
CVE
CVE-2026-3113
Disclosed
Trace
4 steps

Summary

Source

downloadFile

server/cmd/mmctl/commands/export.go:238

Sink

os.Create

server/cmd/mmctl/commands/export.go:250

mmctl export download and related export download flows write potentially sensitive export archives to the administrator's local filesystem. The shared downloadFile helper used os.Create(path) when the destination did not exist, which creates files with mode 0666 before process umask is applied. On a common 022 umask, the resulting export file is 0644 and readable by other local users. When the destination already existed but was empty, os.OpenFile(path, os.O_WRONLY, 0600) did not change existing permissions, so a pre-created permissive file remained permissive.

The fix creates new files with os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600) and explicitly os.Chmod(path, 0600) before opening existing empty files. The original fix is PR #35182 / commit 6cd2df33ea12f9aeae4c68994b72656425168b67; backports include #35244, #35246, #35247, and #35248.

Severity

Attack vectorAV
Local
Attack complexityAC
Low
Privileges requiredPR
Low
User interactionUI
Required
ScopeS
Unchanged
ConfidentialityC
High
IntegrityI
None
AvailabilityA
None

Metric values as published in the disclosure vector. Meters show how far each value raises exposure.

Source-to-sink trace

  1. Source · attacker-controlledserver/cmd/mmctl/commands/export.go:238

    downloadFile

  2. Step 01server/cmd/mmctl/commands/export.go:238-250

    The shared export download helper receives a destination path and checks whether the file exists. Before the fix, the new-file branch used os.Create.

    go
    case err != nil:
        // file does not exist, we create it
        outFile, err = os.Create(path)
        createdFile = true
    
  3. Step 02server/cmd/mmctl/commands/export.go:251-253

    Before the fix, the existing-empty-file branch used os.OpenFile with a mode argument, but that mode is ignored unless a file is created. Existing permissive permissions remained unchanged.

    go
    default:
        // no error, file exists, we open it
        outFile, err = os.OpenFile(path, os.O_WRONLY, 0600)
    
  4. Step 03server/cmd/mmctl/commands/export.go:249-258

    The patched code creates new files as 0600, and fixes existing empty destination files with os.Chmod(path, 0600) before writing export contents.

    go
    case err != nil:
        outFile, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600)
        createdFile = true
    default:
        permErr := os.Chmod(path, 0600)
        if permErr != nil {
            return "", fmt.Errorf("failed to change permissions on output file: %w", permErr)
        }
    
        outFile, err = os.OpenFile(path, os.O_WRONLY, 0600)
    
  5. Step 04server/cmd/mmctl/commands/export_e2e_test.go:232-236

    The regression tests now assert 0600 permissions for both existing-empty-file and full-download paths.

    go
    info, err := os.Stat(downloadPath)
    s.Require().Nil(err)
    s.Require().Equal(fs.FileMode(0600), info.Mode().Perm(), fmt.Sprintf("expected %o, got %o", fs.FileMode(0600), info.Mode().Perm()))
    
  6. Sinkserver/cmd/mmctl/commands/export.go:250

    os.Create

Impact

Reported impact

The server-side export permission model can be bypassed at rest on the operator's local machine. Sensitive export data that only an administrator should access may become readable by other local users or processes.

Attack surface

Administrator workstations or automation hosts where mmctl export download writes Mattermost export archives to shared filesystems or machines with multiple local users.

Preconditions

A privileged operator must download an export file using a vulnerable mmctl build. A local attacker must be able to read files created in the destination directory under the resulting mode.

Attack path

  1. 1

    An administrator runs mmctl export download without specifying a restrictive umask.

  2. 2

    The vulnerable helper creates the local archive with default create permissions masked by umask, often 0644.

  3. 3

    Another local user on the same host reads the export archive from disk.

  4. 4

    The archive may contain message history, files, user data, or compliance content depending on the export.

Proof of concept

Reproduction5 stages
  1. 01

    Environment setup

    Use a vulnerable mmctl build before PR #35182 on a Unix-like system with a typical umask 022.

  2. 02

    Target configuration

    Run an export download into a directory where file mode can be inspected, or pre-create an empty file with permissive permissions and pass it as the download destination.

  3. 03

    Exploit delivery

    Run umask 022 followed by mmctl export download <export.zip> ./export.zip. Then run stat -c %a export.zip or the platform equivalent.

  4. 04

    Expected response

    Vulnerable builds can produce 644 for new files or preserve permissive permissions on existing empty files. Fixed builds produce 600.

  5. 05

    Outcome

    Export archives are no longer readable by other local users by default.

Remediation

Guidance

Create sensitive download files with explicit owner-only permissions, and normalize permissions on existing empty files before writing. Do not rely on process umask or the mode argument to os.OpenFile when the file already exists.

Before and after−7+12
case err != nil:    // file does not exist, we create it    outFile, err = os.Create(path)    createdFile = truedefault:    // no error, file exists, we open it    outFile, err = os.OpenFile(path, os.O_WRONLY, 0600)case err != nil:    // file does not exist, we create it    outFile, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600)    createdFile = truedefault:    // no error, file exists, we double check the permissions and then open it    permErr := os.Chmod(path, 0600)    if permErr != nil {        return "", fmt.Errorf("failed to change permissions on output file: %w", permErr)    }     outFile, err = os.OpenFile(path, os.O_WRONLY, 0600)
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.