mmctl terminal escape injection via unsanitized server-controlled output (CVE-2026-3108)
mmctl rendered user-controlled Mattermost content to administrator terminals without stripping ANSI, OSC, DCS, or other control sequences
Severity rating
HighNo numeric base score was published with this disclosure.
Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:L
Summary
Source
reportPostsCmdF
server/cmd/mmctl/commands/report.go:111
Sink
Printer.linesToBytes
server/cmd/mmctl/printer/printer.go:275
Mattermost's mmctl plain-text output path accumulated values from API responses and templates in printer.Lines, then wrote each line directly to stdout in Printer.linesToBytes. The report-posts command also bypassed the printer and wrote post.Message directly with fmt.Fprintf(os.Stdout, ...). Because post messages and many other displayed fields can be controlled by ordinary users on the server being administered, a malicious value containing ANSI CSI sequences or OSC sequences could be replayed into an administrator's terminal when they inspected data with mmctl.
The root cause was a missing terminal-output trust boundary: the CLI treated server data as display-safe text. The fix added printer.SanitizeForTerminal, applied it to the central plain-output sink in linesToBytes, and separately sanitized the direct report command message print. The original fix is PR #35191 / commit 22e4e9c171dcf9b447a56c7d9a45e5714fb24536; release-line backports include #35208, #35276, #35334, and #35335.
Severity
- Attack vectorAV
- Network
- Attack complexityAC
- Low
- Privileges requiredPR
- Low
- User interactionUI
- Required
- ScopeS
- Changed
- ConfidentialityC
- High
- IntegrityI
- High
- AvailabilityA
- Low
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/ report.go:111 reportPostsCmdF
- Step 01server/
cmd/ mmctl/ commands/ report.go:111-118 The administrator runs
mmctl report posts, which fetches post objects from the Mattermost API and passes every returned post intoprintReportPost. Thepost.Messagefield is user-controlled content stored on the server.go response, _, err := c.GetPostsForReporting(context.TODO(), options, cursorObj) if err != nil { return fmt.Errorf("failed to get posts for reporting: %w", err) } for _, post := range response.Posts { printReportPost(post) } - Step 02server/
cmd/ mmctl/ commands/ report.go:147-152 Before the fix, report output wrote
post.Messagedirectly to stdout. The patched code appliesprinter.SanitizeForTerminalbefore rendering the message.go fmt.Fprintf(os.Stdout, "Post ID: %s\n", post.Id) fmt.Fprintf(os.Stdout, "User ID: %s\n", post.UserId) fmt.Fprintf(os.Stdout, "Channel ID: %s\n", post.ChannelId) fmt.Fprintf(os.Stdout, "Message: %s\n", printer.SanitizeForTerminal(post.Message)) - Step 03server/
cmd/ mmctl/ printer/ printer.go:103-115 Most mmctl commands render API-derived values through templates and append the rendered string into
printer.Lines.go tpl := template.Must(template.New("").Funcs(printer.templateFuncs).Parse(templateString)) sb := &strings.Builder{} if err := tpl.Execute(sb, v); err != nil { PrintError("Can't print the message using the provided template: " + templateString) return } printer.Lines = append(printer.Lines, sb.String()) - Step 04server/
cmd/ mmctl/ printer/ printer.go:272-280 The central plain-text sink now converts each line to a string and strips terminal control sequences before writing to the output buffer.
go case FormatPlain: var buf bytes.Buffer for i := range p.Lines { line := fmt.Sprintf("%s", p.Lines[i]) fmt.Fprintf(&buf, "%s%s", SanitizeForTerminal(line), newline) } b = buf.Bytes() - Step 05server/
cmd/ mmctl/ printer/ printer_helpers.go:27-61 SanitizeForTerminalremoves ANSI CSI, OSC, DCS, APC/PM, single-character escape sequences, and remaining control characters except tab, newline, and carriage return.go func SanitizeForTerminal(s string) string { result := csiRegex.ReplaceAllString(s, "") result = oscRegex.ReplaceAllString(result, "") result = dcsRegex.ReplaceAllString(result, "") result = otherEscRegex.ReplaceAllString(result, "") var cleaned strings.Builder cleaned.Grow(len(result)) for _, r := range result { switch { case r == '\t' || r == '\n' || r == '\r': cleaned.WriteRune(r) case r < 0x20 || r == 0x7F: continue default: cleaned.WriteRune(r) } } return cleaned.String() } - Sinkserver/
cmd/ mmctl/ printer/ printer.go:275 Printer.linesToBytes
Impact
Reported impact
The bug crosses the trust boundary between untrusted server data and the administrator's local terminal. Depending on terminal behavior, payloads can alter terminal state, hide or rewrite displayed output, set misleading prompts or window titles, or abuse OSC features such as clipboard writes. This is not server-side code execution, but it is a high-impact operator-side terminal injection primitive against privileged users.
Attack surface
Administrators or operators running mmctl against a Mattermost instance containing attacker-controlled content, especially commands that print post messages, user fields, channel names, webhook data, or other server-originated strings in plain format.
Preconditions
The attacker must be able to place content that later appears in mmctl output. A privileged user must run an affected mmctl command in a terminal that interprets escape sequences. JSON output is less exposed because JSON encoding escapes control bytes, while plain output was vulnerable.
Attack path
- 1
Store a message or other displayed field containing ANSI/OSC terminal control sequences.
- 2
Wait for an administrator to inspect that object with mmctl in plain output mode.
- 3
The vulnerable code prints the attacker-controlled bytes without terminal sanitization.
- 4
The terminal interprets the sequences, allowing effects such as screen manipulation, deceptive prompts, title changes, or OSC clipboard writes depending on terminal settings.
Proof of concept
- 01
Environment setup
Use a vulnerable Mattermost build before PR #35191 and a terminal that interprets ANSI/OSC sequences. Configure mmctl for an administrator account.
- 02
Target configuration
Any server where an ordinary user can create a post that an administrator later reviews with
mmctl report postsor another plain-output mmctl command. - 03
Exploit delivery
Create a post containing terminal control bytes such as CSI screen-clear sequences or an OSC clipboard sequence. Then run
mmctl report postsas an administrator and include the malicious post in the report results. - 04
Expected response
On vulnerable builds, the terminal receives the raw escape bytes from
post.Messageor fromprinter.Lines. On fixed builds, the same output is rendered with the escape/control sequences removed. - 05
Outcome
The fix prevents terminal interpretation by sanitizing both the central printer path and the direct report-post message output.
Remediation
Guidance
Treat all Mattermost server data as untrusted when rendering to an administrator terminal. Strip ANSI/OSC/DCS/control sequences at the central printer sink, and sanitize any command that bypasses that sink and writes directly to stdout.
case FormatPlain:var buf bytes.Bufferfor i := range p.Lines {fmt.Fprintf(&buf, "%s%s", p.Lines[i], newline)}b = buf.Bytes()// report.go also wrote raw post messages:fmt.Fprintf(os.Stdout, "Message: %s\n", post.Message)case FormatPlain: var buf bytes.Buffer for i := range p.Lines { line := fmt.Sprintf("%s", p.Lines[i]) fmt.Fprintf(&buf, "%s%s", SanitizeForTerminal(line), newline) } b = buf.Bytes() // report.go now sanitizes the direct stdout path:fmt.Fprintf(os.Stdout, "Message: %s\n", printer.SanitizeForTerminal(post.Message))
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.
