User-Agent version parser panic during session creation (CVE-2026-25783)
Malformed Mattermost-specific User-Agent prefixes could panic getBrowserVersion
CVSS 3.1 base score
MediumVector: 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
- Source · attacker-controlledserver/
channels/ app/ login.go:170 App.DoLogin
- Step 01server/
channels/ api4/ user.go:2129-2130 After authentication succeeds, the login handler calls
DoLoginwith the original HTTP request, including the attacker-controlledUser-Agentheader.go isMobileDevice := utils.IsMobileRequest(r) session, err := c.App.DoLogin(c.AppContext, w, r, user, deviceId, isMobileDevice, false, false) - Step 02server/
channels/ app/ login.go:170-179 DoLoginparsesr.UserAgent()and asksgetBrowserVersionfor 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)) - 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) } - 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 togetUAVersion.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) } - 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
Authenticate with a header such as
User-Agent: Mattermost Mobile/orUser-Agent: mmctl/. - 2
The login flow reaches
DoLoginafter successful authentication. - 3
The vulnerable parser finds the prefix, produces an empty
strings.Fieldsresult, and indexes[0]. - 4
The request panics, causing a 500 response and noisy server-side panic handling.
Proof of concept
- 01
Environment setup
Use a vulnerable build before PR #35098 and an account that can log in successfully.
- 02
Target configuration
No special server configuration is required beyond a reachable login flow.
- 03
Exploit delivery
Send a valid login request with
User-Agent: Mattermost Mobile/orUser-Agent: mmctl/and correct credentials. - 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. - 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.
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)}
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.
