Skip to content
winfunc
Disclosure record · Mattermost

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

MattermostCVE-2026-3108DisclosedSource record

Severity rating

High
High

No 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

Record

Project
Mattermost
Severity
High
CVE
CVE-2026-3108
Disclosed
Trace
5 steps

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

  1. Source · attacker-controlledserver/cmd/mmctl/commands/report.go:111

    reportPostsCmdF

  2. 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 into printReportPost. The post.Message field 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)
    }
    
  3. Step 02server/cmd/mmctl/commands/report.go:147-152

    Before the fix, report output wrote post.Message directly to stdout. The patched code applies printer.SanitizeForTerminal before 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))
    
  4. 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())
    
  5. 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()
    
  6. Step 05server/cmd/mmctl/printer/printer_helpers.go:27-61

    SanitizeForTerminal removes 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()
    }
    
  7. 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. 1

    Store a message or other displayed field containing ANSI/OSC terminal control sequences.

  2. 2

    Wait for an administrator to inspect that object with mmctl in plain output mode.

  3. 3

    The vulnerable code prints the attacker-controlled bytes without terminal sanitization.

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

Reproduction5 stages
  1. 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.

  2. 02

    Target configuration

    Any server where an ordinary user can create a post that an administrator later reviews with mmctl report posts or another plain-output mmctl command.

  3. 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 posts as an administrator and include the malicious post in the report results.

  4. 04

    Expected response

    On vulnerable builds, the terminal receives the raw escape bytes from post.Message or from printer.Lines. On fixed builds, the same output is rendered with the escape/control sequences removed.

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

Before and after−9+10
case FormatPlain:    var buf bytes.Buffer    for 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))
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.