Skip to content
winfunc
Disclosure record · Mattermost

User-Agent version parser panic during session creation (CVE-2026-25783)

Malformed Mattermost-specific User-Agent prefixes could panic getBrowserVersion

MattermostCVE-2026-25783DisclosedSource 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-25783
Disclosed
Trace
4 steps

Summary

Source

App.DoLogin

server/channels/app/login.go:170

Sink

getBrowserVersion

server/channels/app/user_agent.go:134

DoLogin parses the HTTP User-Agent header while creating a session and stores platform, OS, browser name, and browser version on the session. The version helper looked for Mattermost-specific prefixes such as Mattermost Mobile/, Mattermost/, mmctl/, and Franz/, then immediately returned strings.Fields(afterVersion)[0]. If the header ended at the prefix or contained only whitespace after it, strings.Fields returned an empty slice and the [0] index panicked.

This is not reached before authentication in the normal password login flow: login calls AuthenticateUserForLogin first, then DoLogin. A malicious client needs a way to complete session creation, such as a valid account or an enabled registration/login flow. The fix centralizes prefixes in versionPrefixes, checks len(fields) > 0 before indexing, and falls back to the parsed user-agent version. The original fix is PR #35098 / commit 1346cf529aef0672c39a56ec10d1b8a9c8fb387d; backports include #35124, #35131, #35132, and #35133.

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/login.go:170

    App.DoLogin

  2. Step 01server/channels/api4/user.go:2129-2130

    After authentication succeeds, the login handler calls DoLogin with the original HTTP request, including the attacker-controlled User-Agent header.

    go
    isMobileDevice := utils.IsMobileRequest(r)
    session, err := c.App.DoLogin(c.AppContext, w, r, user, deviceId, isMobileDevice, false, false)
    
  3. Step 02server/channels/app/login.go:170-179

    DoLogin parses r.UserAgent() and asks getBrowserVersion for the version string that will be stored in the session props.

    go
    ua := uasurfer.Parse(r.UserAgent())
    
    plat := getPlatformName(ua, r.UserAgent())
    os := getOSName(ua, r.UserAgent())
    bname := getBrowserName(ua, r.UserAgent())
    bversion := getBrowserVersion(ua, r.UserAgent())
    
    session.AddProp(model.SessionPropBrowser, fmt.Sprintf("%v/%v", bname, bversion))
    
  4. Step 03server/channels/app/user_agent.go:107-124 (before fix)

    Before the patch, each special-case prefix indexed strings.Fields(afterVersion)[0] without confirming any field existed.

    go
    if index := strings.Index(userAgentString, "Mattermost Mobile/"); index != -1 {
        afterVersion := userAgentString[index+len("Mattermost Mobile/"):]
        return limitStringLength(strings.Fields(afterVersion)[0], maxUserAgentVersionLength)
    }
    
  5. Step 04server/channels/app/user_agent.go:112-143

    The patched version loops over known prefixes and only indexes the first field when len(fields) > 0; otherwise it falls back to getUAVersion.

    go
    var versionPrefixes = []string{
        "Mattermost Mobile/",
        desktopAppVersionPrefix,
        "mmctl/",
        "Franz/",
    }
    
    func getBrowserVersion(ua *uasurfer.UserAgent, userAgentString string) string {
        for _, prefix := range versionPrefixes {
            if _, after, ok := strings.Cut(userAgentString, prefix); ok {
                if fields := strings.Fields(after); len(fields) > 0 {
                    return limitStringLength(fields[0], maxUserAgentVersionLength)
                }
            }
        }
        return getUAVersion(ua.Browser.Version)
    }
    
  6. Sinkserver/channels/app/user_agent.go:134

    getBrowserVersion

Impact

Reported impact

Repeated successful login attempts with malformed User-Agent headers can generate panics and degrade availability of login/session creation. The blast radius is bounded by the requirement to reach DoLogin, but the missing bounds check is a server-side panic on attacker-controlled input.

Attack surface

Session creation paths that call DoLogin, including the password login endpoint after successful authentication and other login flows that create a session.

Preconditions

The attacker must be able to complete login/session creation, for example with their own valid account. They control the User-Agent header.

Attack path

  1. 1

    Authenticate with a header such as User-Agent: Mattermost Mobile/ or User-Agent: mmctl/.

  2. 2

    The login flow reaches DoLogin after successful authentication.

  3. 3

    The vulnerable parser finds the prefix, produces an empty strings.Fields result, and indexes [0].

  4. 4

    The request panics, causing a 500 response and noisy server-side panic handling.

Proof of concept

Reproduction5 stages
  1. 01

    Environment setup

    Use a vulnerable build before PR #35098 and an account that can log in successfully.

  2. 02

    Target configuration

    No special server configuration is required beyond a reachable login flow.

  3. 03

    Exploit delivery

    Send a valid login request with User-Agent: Mattermost Mobile/ or User-Agent: mmctl/ and correct credentials.

  4. 04

    Expected response

    Vulnerable builds panic with an index-out-of-range error in getBrowserVersion. Fixed builds complete the request and use the fallback parsed browser version when no prefix token exists.

  5. 05

    Outcome

    Malformed User-Agent prefixes no longer crash session creation.

Remediation

Guidance

Never index parsed tokens from attacker-controlled headers before checking length. Centralizing known prefixes also reduces the chance of adding a future prefix with the same unchecked indexing bug.

Before and after−13+11
func getBrowserVersion(ua *uasurfer.UserAgent, userAgentString string) string {    if index := strings.Index(userAgentString, "Mattermost Mobile/"); index != -1 {        afterVersion := userAgentString[index+len("Mattermost Mobile/"):]        return limitStringLength(strings.Fields(afterVersion)[0], maxUserAgentVersionLength)    }     if index := strings.Index(userAgentString, "Mattermost/"); index != -1 {        afterVersion := userAgentString[index+len("Mattermost/"):]        return limitStringLength(strings.Fields(afterVersion)[0], maxUserAgentVersionLength)    }     return getUAVersion(ua.Browser.Version)}func getBrowserVersion(ua *uasurfer.UserAgent, userAgentString string) string {    for _, prefix := range versionPrefixes {        if index := strings.Index(userAgentString, prefix); index != -1 {            afterPrefix := userAgentString[index+len(prefix):]            if fields := strings.Fields(afterPrefix); len(fields) > 0 {                return limitStringLength(fields[0], maxUserAgentVersionLength)            }        }    }    return getUAVersion(ua.Browser.Version)}
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.