mmctl export downloads created world-readable local files (CVE-2026-3113)
Export files downloaded with mmctl inherited unsafe filesystem permissions
Severity rating
MediumNo 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
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
- Source · attacker-controlledserver/
cmd/ mmctl/ commands/ export.go:238 downloadFile
- 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 - Step 02server/
cmd/ mmctl/ commands/ export.go:251-253 Before the fix, the existing-empty-file branch used
os.OpenFilewith 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) - Step 03server/
cmd/ mmctl/ commands/ export.go:249-258 The patched code creates new files as
0600, and fixes existing empty destination files withos.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) - Step 04server/
cmd/ mmctl/ commands/ export_e2e_test.go:232-236 The regression tests now assert
0600permissions 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())) - 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
An administrator runs
mmctl export downloadwithout specifying a restrictive umask. - 2
The vulnerable helper creates the local archive with default create permissions masked by umask, often
0644. - 3
Another local user on the same host reads the export archive from disk.
- 4
The archive may contain message history, files, user data, or compliance content depending on the export.
Proof of concept
- 01
Environment setup
Use a vulnerable mmctl build before PR #35182 on a Unix-like system with a typical
umask 022. - 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.
- 03
Exploit delivery
Run
umask 022followed bymmctl export download <export.zip> ./export.zip. Then runstat -c %a export.zipor the platform equivalent. - 04
Expected response
Vulnerable builds can produce
644for new files or preserve permissive permissions on existing empty files. Fixed builds produce600. - 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.
case err != nil:// file does not exist, we create itoutFile, err = os.Create(path)createdFile = truedefault:// no error, file exists, we open itoutFile, 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)
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.
