Skip to content
winfunc
Disclosure record · Mattermost

SSRF protection bypass via IPv4-mapped IPv6 literals (CVE-2026-2455)

IPv4-mapped IPv6 addresses were not canonicalized before reserved-range checks

MattermostCVE-2026-2455DisclosedSource record

CVSS 3.1 base score

Medium
6.5/ 10

Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

Record

Project
Mattermost
Severity
Medium6.5
CVE
CVE-2026-2455
Disclosed
Trace
4 steps

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

  1. Source · attacker-controlledserver/channels/app/post_metadata.go:1021

    App.getLinkMetadataForURL

  2. 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)
    
  3. Step 02server/public/shared/httpservice/httpservice.go:88-115

    MakeClient(false) installs an allowIP callback 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)
    }
    
  4. 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
        }
    }
    
  5. 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.1 is evaluated as 127.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
    }
    
  6. 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. 1

    Submit a URL whose host is an IPv4-mapped IPv6 literal, such as http://[::ffff:127.0.0.1]:8065/ or http://[::ffff:169.254.169.254]/.

  2. 2

    Mattermost resolves/checks the address through the shared HTTP transport.

  3. 3

    The vulnerable IsReservedIP compares the mapped IPv6 value against IPv4 reserved CIDRs without canonicalizing it.

  4. 4

    The transport treats the address as allowed and dials the internal IPv4 target.

Proof of concept

Reproduction5 stages
  1. 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.

  2. 02

    Target configuration

    Run Mattermost with default internal-connection protections. Have an authenticated user capable of posting a link in a channel.

  3. 03

    Exploit delivery

    Post a message containing a URL such as http://[::ffff:127.0.0.1]:8065/api/v4/system/ping or another internal IPv4 target encoded as an IPv4-mapped IPv6 literal.

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

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

Before and after−8+13
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}
View original source record

Check the upstream record for the project's current remediation status.

Your codebase

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.