SSRF protection bypass via IPv4-mapped IPv6 literals (CVE-2026-2455)
IPv4-mapped IPv6 addresses were not canonicalized before reserved-range checks
CVSS 3.1 base score
MediumVector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
Summary
Source
App.getLinkMetadataForURL
server/channels/app/post_metadata.go:1021
Sink
IsReservedIP
server/public/shared/httpservice/client.go:30
Mattermost's shared HTTP service protects untrusted outbound requests by resolving the target host and passing each IP through allowIP, which calls IsReservedIP and IsOwnIP. IsReservedIP contained IPv4 private, loopback, and link-local CIDRs, but it compared the raw net.IP value directly against those ranges. For an address such as ::ffff:127.0.0.1, Go represents the value as an IPv4-mapped IPv6 address. Without canonicalization, the intended IPv4 reserved ranges could be bypassed and the request could be allowed even though the effective target is loopback or another internal IPv4 address.
The fix calls ip.To4() at the start of IsReservedIP and, when it returns non-nil, compares the canonical native IPv4 bytes against the reserved ranges. The original fix is PR #35097 / commit 5d787969c2d5ab591a9dcd61b0810475eed7a646; backports include #35122, #35128, #35129, and #35130.
Severity
- Attack vectorAV
- Network
- Attack complexityAC
- Low
- Privileges requiredPR
- Low
- User interactionUI
- None
- ScopeS
- Unchanged
- ConfidentialityC
- High
- IntegrityI
- None
- AvailabilityA
- None
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/ post_metadata.go:1021 App.getLinkMetadataForURL
- Step 01server/
channels/ app/ post_metadata.go:1021-1040 Link preview/OpenGraph metadata fetching is one reachable path for user-controlled URLs. It creates a protected HTTP client with
MakeClient(false).go func (a *App) getLinkMetadataForURL(rctx request.CTX, requestURL string) (*opengraph.OpenGraph, *model.PostImage, error) { // ... client := a.HTTPService().MakeClient(false) - Step 02server/
public/ shared/ httpservice/ httpservice.go:88-115 MakeClient(false)installs anallowIPcallback that rejects reserved or self-assigned IPs unless explicitly allowed.go allowIP := func(ip net.IP) error { reservedIP := IsReservedIP(ip) ownIP, err := IsOwnIP(ip) if err != nil { return fmt.Errorf("unable to determine if IP is own IP: %w", err) } if !reservedIP && !ownIP { return nil } if reservedIP { return fmt.Errorf("IP %s is in a reserved range and not in AllowedUntrustedInternalConnections", ip) } return fmt.Errorf("IP %s is a self-assigned IP and not in AllowedUntrustedInternalConnections", ip) } - Step 03server/
public/ shared/ httpservice/ client.go:129-164 The transport resolves the hostname, checks each IP with
allowIP, and dials the first allowed address.go ips, err := net.LookupIP(host) if err != nil { return nil, err } for _, ip := range ips { if err := allowIP(ip); err != nil { forbiddenReasons = append(forbiddenReasons, err.Error()) continue } conn, err := dial(ctx, network, net.JoinHostPort(ip.String(), port)) if err == nil { return conn, nil } } - Step 04server/
public/ shared/ httpservice/ client.go:30-41 The patch canonicalizes IPv4-mapped IPv6 addresses before checking reserved CIDRs, so
::ffff:127.0.0.1is evaluated as127.0.0.1.go func IsReservedIP(ip net.IP) bool { if ip4 := ip.To4(); ip4 != nil { ip = ip4 } for _, ipRange := range reservedIPRanges { if ipRange.Contains(ip) { return true } } return false } - Sinkserver/
public/ shared/ httpservice/ client.go:30 IsReservedIP
Impact
Reported impact
The bypass can expose internal-only services reachable from the Mattermost server, including loopback services, RFC1918 resources, or cloud metadata endpoints, through any feature that returns or processes fetched metadata. The practical data exposure depends on the specific URL-fetching feature and response handling.
Attack surface
Features that fetch untrusted URLs through HTTPService().MakeClient(false), including link metadata, image proxy paths, marketplace access, SAML metadata fetches, and integrations depending on configuration and caller permissions.
Preconditions
The attacker must reach a feature that causes Mattermost to fetch an attacker-supplied URL through the protected HTTP client. In common chat paths this requires an authenticated user who can post a link or otherwise submit a URL.
Attack path
- 1
Submit a URL whose host is an IPv4-mapped IPv6 literal, such as
http://[::ffff:127.0.0.1]:8065/orhttp://[::ffff:169.254.169.254]/. - 2
Mattermost resolves/checks the address through the shared HTTP transport.
- 3
The vulnerable
IsReservedIPcompares the mapped IPv6 value against IPv4 reserved CIDRs without canonicalizing it. - 4
The transport treats the address as allowed and dials the internal IPv4 target.
Proof of concept
- 01
Environment setup
Use a vulnerable build before PR #35097 and a feature that fetches untrusted URLs through
MakeClient(false), such as link preview metadata. - 02
Target configuration
Run Mattermost with default internal-connection protections. Have an authenticated user capable of posting a link in a channel.
- 03
Exploit delivery
Post a message containing a URL such as
http://[::ffff:127.0.0.1]:8065/api/v4/system/pingor another internal IPv4 target encoded as an IPv4-mapped IPv6 literal. - 04
Expected response
Vulnerable builds allow the outbound connection to the internal IPv4 target. Fixed builds reject the address as reserved unless explicitly configured in
AllowedUntrustedInternalConnections. - 05
Outcome
IPv4-mapped IPv6 literals are evaluated using the same reserved-range policy as their native IPv4 equivalents.
Remediation
Guidance
Canonicalize IP addresses before applying reserved-range logic. In Go, net.IP.To4() returns the effective IPv4 bytes for IPv4 and IPv4-mapped IPv6 addresses, allowing existing IPv4 CIDRs to match correctly.
func IsReservedIP(ip net.IP) bool {for _, ipRange := range reservedIPRanges {if ipRange.Contains(ip) {return true}}return false}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}
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.
