Skip to content

IPv6 transition address bypass of webhook URL SSRF guard

Moderate
snipe published GHSA-5j6m-rr83-rpj7 Aug 24, 2026

Package

No package listed

Affected versions

unreleased master

Patched versions

8.7.0

Description

IPv6 transition address bypass of webhook URL SSRF guard

Summary

The ExternalUrl validation rule in app/Rules/ExternalUrl.php prevents webhook URLs from targeting private/internal addresses. The isPublicIp method unwraps IPv4-mapped IPv6 (::ffff:x.x.x.x) and then relies on PHP's filter_var with FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE to reject non-public addresses.

PHP's built-in filter flags do not classify IPv6 transition addresses as private or reserved. Three families of transition mechanisms encode a private IPv4 target inside a globally-routable IPv6 wrapper, and all three pass through filter_var unchecked.

Affected code

app/Rules/ExternalUrl.php lines 72-88:

private function isPublicIp(string $ip): bool
{
    // Unwrap IPv4-mapped IPv6 so ::ffff:127.0.0.1 doesn't sneak past
    // the IPv4 private/reserved checks.
    if (stripos($ip, '::ffff:') === 0) {
        $ipv4 = substr($ip, 7);
        if (filter_var($ipv4, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            $ip = $ipv4;
        }
    }

    return (bool) filter_var(
        $ip,
        FILTER_VALIDATE_IP,
        FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
    );
}

PHP's FILTER_FLAG_NO_PRIV_RANGE blocks fc00::/7 (unique local) only. FILTER_FLAG_NO_RES_RANGE blocks ::1, ::, ::ffff:0:0/96, fe80::/10, 2001:db8::/32, and ff00::/8. None of these cover:

Mechanism Prefix RFC Embedding
NAT64 well-known prefix 64:ff9b::/96 RFC 6052 Last 32 bits = IPv4
6to4 2002::/16 RFC 3056 Bits 16-47 = IPv4
Teredo 2001:0000::/32 RFC 4380 Last 32 bits = IPv4 XOR 0xffffffff

Bypass vectors

A super-admin configuring a webhook URL can supply an IPv6 literal containing a transition address. Since IPv6 literals are valid IP addresses, they go directly to isPublicIp without DNS resolution.

NAT64 -- targeting AWS IMDS 169.254.169.254

http://[64:ff9b::a9fe:a9fe]/latest/meta-data/
http://[64:ff9b::169.254.169.254]/latest/meta-data/

filter_var('64:ff9b::a9fe:a9fe', FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) returns '64:ff9b::a9fe:a9fe' (passes validation). On a NAT64-enabled host, this routes to 169.254.169.254.

6to4 -- targeting localhost 127.0.0.1

http://[2002:7f00:1::]/
http://[2002:a9fe:a9fe::]/latest/meta-data/

2002:7f00:1:: embeds 127.0.0.1 in bits 16-47. Passes filter_var.

Teredo -- targeting 10.0.0.1

http://[2001:0000:4136:e378:8000:63bf:f5ff:fffe]/

Last 32 bits f5ff:fffe XOR ffff:ffff = 0a00:0001 = 10.0.0.1. Passes filter_var.

DNS resolution path

The same bypass applies through DNS resolution. If an attacker controls a DNS zone, they can return an AAAA record containing a transition address for any hostname. The resolveHost method uses dns_get_record($host, DNS_AAAA) and passes each resolved IPv6 address to isPublicIp, where filter_var again fails to detect the embedded private IPv4.

Impact

A super-admin who sets a webhook URL to a transition address (or a domain resolving to one) can trigger SSRF on a NAT64/6to4/Teredo-enabled host:

  • Reading cloud instance metadata (AWS/GCP/Azure IMDS at 169.254.169.254)
  • Accessing internal services on private networks
  • Port scanning internal infrastructure

Suggested fix

Extract the embedded IPv4 from transition addresses and re-validate:

private function isPublicIp(string $ip): bool
{
    if (stripos($ip, '::ffff:') === 0) {
        $ipv4 = substr($ip, 7);
        if (filter_var($ipv4, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            $ip = $ipv4;
        }
    }

    // Extract embedded IPv4 from IPv6 transition addresses
    $embedded = $this->extractTransitionIpv4($ip);
    if ($embedded !== null) {
        return $this->isPublicIp($embedded);
    }

    return (bool) filter_var(
        $ip,
        FILTER_VALIDATE_IP,
        FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
    );
}

private function extractTransitionIpv4(string $ip): ?string
{
    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === false) {
        return null;
    }

    $packed = @inet_pton($ip);
    if ($packed === false || strlen($packed) !== 16) {
        return null;
    }

    $words = array_values(unpack('n8', $packed));

    // NAT64 64:ff9b::/96
    if ($words[0] === 0x0064 && $words[1] === 0xff9b
        && $words[2] === 0 && $words[3] === 0 && $words[4] === 0 && $words[5] === 0) {
        return long2ip(($words[6] << 16) | $words[7]);
    }
    // 6to4 2002::/16
    if ($words[0] === 0x2002) {
        return long2ip(($words[1] << 16) | $words[2]);
    }
    // Teredo 2001:0000::/32
    if ($words[0] === 0x2001 && $words[1] === 0x0000) {
        return long2ip((($words[6] ^ 0xffff) << 16) | ($words[7] ^ 0xffff));
    }

    return null;
}

Credit

tonghuaroot (https://github.com/tonghuaroot)

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
High
Privileges required
High
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
Low
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:L/A:N

CVE ID

No known CVE

Weaknesses

Server-Side Request Forgery (SSRF)

The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination. Learn more on MITRE.

Credits