Every CVE Search/ Get API key

CVE-2026-54494

Koel: Full-read SSRF via podcast enclosure URL: isPublicHost() filter_var guard does not reject NAT64 (64:ff9b::/96) or 6to4 (2002::/16) IPv6-transition wrappers of internal IPv4

5.3 MediumFix available

Koel is a free, open-source music streaming solution. Prior to 9.7.1, App\Helpers\Network::isPublicHost() uses filter_var() with FILTER_FLAG_NO_PRIV_RANGE and FILTER_FLAG_NO_RES_RANGE, which treats NAT64 64:ff9b::/96 and 6to4 2002::/16 wrappers of private, loopback, or link-local IPv4 addresses as public. An authenticated user can place such an address in a podcast RSS <enclosure url> and reach app/Values/Podcast/EpisodePlayable.php through EpisodePlayable::createForEpisode(), where isSafeUrl() accepts the target and Http::sink($file)->get($url) fetches it. On a host with NAT64 or 6to4 routing, Koel can request internal services or cloud metadata and return the response body to the user. This issue is fixed in version 9.7.1

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
Low
Privileges needed
Low
User interaction
None

What an attacker can affect

Confidentiality
Low
Integrity
Low
Availability
None

Exploit likelihood

0.43%Up 0.2 points since Aug 20, 2026
90%50%10%1%

What each source reports

CVE List Score used 5.3

Koel: Full-read SSRF via podcast enclosure URL: isPublicHost() filter_var guard does not reject NAT64 (64:ff9b::/96) or 6to4 (2002::/16) IPv6-transition wrappers of internal IPv4

Koel is a free, open-source music streaming solution. Prior to 9.7.1, App\Helpers\Network::isPublicHost() uses filter_var() with FILTER_FLAG_NO_PRIV_RANGE and FILTER_FLAG_NO_RES_RANGE, which treats NAT64 64:ff9b::/96 and 6to4 2002::/16 wrappers of private, loopback, or link-local IPv4 addresses as public. An authenticated user can place such an address in a podcast RSS <enclosure url> and reach app/Values/Podcast/EpisodePlayable.php through EpisodePlayable::createForEpisode(), where isSafeUrl() accepts the target and Http::sink($file)->get($url) fetches it. On a host with NAT64 or 6to4 routing, Koel can request internal services or cloud metadata and return the response body to the user. This issue is fixed in version 9.7.1

Record
CVE-2026-54494
CVSS 4.0
5.3 MediumCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Weakness
CWE-918
Updated
2026-11-02
EUVDEUVD-2026-63084 5.3

Koel is a free, open-source music streaming solution. Prior to 9.7.1, App\Helpers\Network::isPublicHost() uses filter_var() with FILTER_FLAG_NO_PRIV_RANGE and FILTER_FLAG_NO_RES_RANGE, which treats NAT64 64:ff9b::/96 and 6to4 2002::/16 wrappers of private, loopback, or link-local IPv4 addresses as public. An authenticated user can place such an address in a podcast RSS <enclosure url> and reach app/Values/Podcast/EpisodePlayable.php through EpisodePlayable::createForEpisode(), where isSafeUrl() accepts the target and Http::sink($file)->get($url) fetches it. On a host with NAT64 or 6to4 routing, Koel can request internal services or cloud metadata and return the response body to the user. This issue is fixed in version 9.7.1

Record
EUVD-2026-63084
CVSS 4.0
5.3 MediumCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Updated
2026-11-02
GitHub AdvisoriesGHSA-rjg7-r26h-cfp2 5.3

Koel: Full-read SSRF via podcast enclosure URL: isPublicHost() filter_var guard does not reject NAT64 (64:ff9b::/96) or 6to4 (2002::/16) IPv6-transition wrappers of internal IPv4

Summary

Koel's outbound-URL guard App\Helpers\Network::isPublicHost() classifies an IP as "public" using PHP's filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE). That flag set does not recognise IPv6 transition-address forms that embed a private/loopback/link-local IPv4: NAT64 well-known prefix 64:ff9b::/96 (RFC 6052) and 6to4 2002::/16 (RFC 3056). An address such as 64:ff9b::7f00:1 (= 127.0.0.1), 64:ff9b::a9fe:a9fe (= 169.254.169.254, the cloud metadata endpoint), or 2002:a00:1:: (= 10.0.0.1) is reported as a public address, so the guard returns true and Koel proceeds to fetch the URL.

The guard is the only SSRF defense in front of App\Values\Podcast\EpisodePlayable::createForEpisode(), which downloads a podcast episode with Http::sink($file)->get($url) and streams the response body back to the requesting user. Because an attacker fully controls the <enclosure url> of any RSS feed they host (and any authenticated user can subscribe to a feed), they can publish an enclosure whose hostname has an AAAA record that is a NAT64/6to4 wrapper of an internal IP. On hosts with NAT64 or 6to4/dual-stack routing (the standard configuration on IPv6-only AWS/GCP subnets and 6to4-relayed networks), the kernel routes the wrapper to the embedded IPv4, and Koel performs a full-read SSRF against the internal endpoint — returning the response body to the attacker.

This is a server-side request forgery with full response disclosure (CWE-918) against internal services and cloud instance metadata.

Vulnerable code

app/Helpers/Network.php — isPublicHost() (the literal-IP branch and the per-resolved-record branch use the identical predicate):

public function isPublicHost(string $host): bool
{
    if (filter_var($host, FILTER_VALIDATE_IP)) {
        return (
            filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false
        );
    }

    try {
        $records = array_merge(dns_get_record($host, DNS_A) ?: [], dns_get_record($host, DNS_AAAA) ?: []);
    } catch (Throwable) {
        return false;
    }

    if ($records === []) {
        return false;
    }

    foreach ($records as $record) {
        $ip = $record['ip'] ?? $record['ipv6'] ?? null;

        if (
            !$ip
            || filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
        ) {
            return false;
        }
    }

    return true;
}

PHP's FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE rejects RFC 1918, loopback, link-local and IPv4-mapped IPv6 (::ffff:a.b.c.d), but treats NAT64 64:ff9b::/96 and 6to4 2002::/16 as ordinary global addresses — even though both forms deterministically embed an IPv4 the kernel will route to.

The sink, app/Values/Podcast/EpisodePlayable.php — createForEpisode():

$network = app(Network::class);
$url = (string) $episode->path;

if (!$network->isSafeUrl($url)) {            // isSafeUrl() -> isPublicHost(), the only guard
    throw UnsafeUrlException::forUrl($url);
}

Http::sink($file)
    ->withOptions([
        'allow_redirects' => [
            'max' => 5,
            'on_redirect' => static function (
                RequestInterface $request,
                ResponseInterface $response,
                UriInterface $uri,
            ) use ($network): void {
                if (!$network->isSafeUrl((string) $uri)) {   // same guard on redirects -> same bypass
                    throw UnsafeUrlException::forUrl((string) $uri);
                }
            },
        ],
    ])
    ->get($url)                              // full-read SSRF: response streamed into $file
    ->throw();

$episode->path is the <enclosure url> from the subscribed podcast RSS feed. The redirect callback reuses the same isSafeUrl(), so a redirect to a NAT64/6to4 host is also accepted.

Attack scenario / How input…

Record
GHSA-rjg7-r26h-cfp2
CVSS 4.0
5.3 MediumCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Weakness
CWE-918
Updated
2026-11-02
NVD 5.3

Koel is a free, open-source music streaming solution. Prior to 9.7.1, App\Helpers\Network::isPublicHost() uses filter_var() with FILTER_FLAG_NO_PRIV_RANGE and FILTER_FLAG_NO_RES_RANGE, which treats NAT64 64:ff9b::/96 and 6to4 2002::/16 wrappers of private, loopback, or link-local IPv4 addresses as public. An authenticated user can place such an address in a podcast RSS <enclosure url> and reach app/Values/Podcast/EpisodePlayable.php through EpisodePlayable::createForEpisode(), where isSafeUrl() accepts the target and Http::sink($file)->get($url) fetches it. On a host with NAT64 or 6to4 routing, Koel can request internal services or cloud metadata and return the response body to the user. This issue is fixed in version 9.7.1

Record
CVE-2026-54494
CVSS 4.0
5.3 MediumCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
Weakness
CWE-918
Updated
2026-11-02
OSV 5.3

Koel: Full-read SSRF via podcast enclosure URL: isPublicHost() filter_var guard does not reject NAT64 (64:ff9b::/96) or 6to4 (2002::/16) IPv6-transition wrappers of internal IPv4

Koel is a free, open-source music streaming solution. Prior to 9.7.1, App\Helpers\Network::isPublicHost() uses filter_var() with FILTER_FLAG_NO_PRIV_RANGE and FILTER_FLAG_NO_RES_RANGE, which treats NAT64 64:ff9b::/96 and 6to4 2002::/16 wrappers of private, loopback, or link-local IPv4 addresses as public. An authenticated user can place such an address in a podcast RSS <enclosure url> and reach app/Values/Podcast/EpisodePlayable.php through EpisodePlayable::createForEpisode(), where isSafeUrl() accepts the target and Http::sink($file)->get($url) fetches it. On a host with NAT64 or 6to4 routing, Koel can request internal services or cloud metadata and return the response body to the user. This issue is fixed in version 9.7.1

Record
CVE-2026-54494
CVSS 4.0
5.3 MediumCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Weakness
CWE-918
Updated
2026-11-02
OSVGHSA-rjg7-r26h-cfp2 5.3

Koel: Full-read SSRF via podcast enclosure URL: isPublicHost() filter_var guard does not reject NAT64 (64:ff9b::/96) or 6to4 (2002::/16) IPv6-transition wrappers of internal IPv4

Summary

Koel's outbound-URL guard App\Helpers\Network::isPublicHost() classifies an IP as "public" using PHP's filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE). That flag set does not recognise IPv6 transition-address forms that embed a private/loopback/link-local IPv4: NAT64 well-known prefix 64:ff9b::/96 (RFC 6052) and 6to4 2002::/16 (RFC 3056). An address such as 64:ff9b::7f00:1 (= 127.0.0.1), 64:ff9b::a9fe:a9fe (= 169.254.169.254, the cloud metadata endpoint), or 2002:a00:1:: (= 10.0.0.1) is reported as a public address, so the guard returns true and Koel proceeds to fetch the URL.

The guard is the only SSRF defense in front of App\Values\Podcast\EpisodePlayable::createForEpisode(), which downloads a podcast episode with Http::sink($file)->get($url) and streams the response body back to the requesting user. Because an attacker fully controls the <enclosure url> of any RSS feed they host (and any authenticated user can subscribe to a feed), they can publish an enclosure whose hostname has an AAAA record that is a NAT64/6to4 wrapper of an internal IP. On hosts with NAT64 or 6to4/dual-stack routing (the standard configuration on IPv6-only AWS/GCP subnets and 6to4-relayed networks), the kernel routes the wrapper to the embedded IPv4, and Koel performs a full-read SSRF against the internal endpoint — returning the response body to the attacker.

This is a server-side request forgery with full response disclosure (CWE-918) against internal services and cloud instance metadata.

Vulnerable code

app/Helpers/Network.php — isPublicHost() (the literal-IP branch and the per-resolved-record branch use the identical predicate):

public function isPublicHost(string $host): bool
{
    if (filter_var($host, FILTER_VALIDATE_IP)) {
        return (
            filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false
        );
    }

    try {
        $records = array_merge(dns_get_record($host, DNS_A) ?: [], dns_get_record($host, DNS_AAAA) ?: []);
    } catch (Throwable) {
        return false;
    }

    if ($records === []) {
        return false;
    }

    foreach ($records as $record) {
        $ip = $record['ip'] ?? $record['ipv6'] ?? null;

        if (
            !$ip
            || filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
        ) {
            return false;
        }
    }

    return true;
}

PHP's FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE rejects RFC 1918, loopback, link-local and IPv4-mapped IPv6 (::ffff:a.b.c.d), but treats NAT64 64:ff9b::/96 and 6to4 2002::/16 as ordinary global addresses — even though both forms deterministically embed an IPv4 the kernel will route to.

The sink, app/Values/Podcast/EpisodePlayable.php — createForEpisode():

$network = app(Network::class);
$url = (string) $episode->path;

if (!$network->isSafeUrl($url)) {            // isSafeUrl() -> isPublicHost(), the only guard
    throw UnsafeUrlException::forUrl($url);
}

Http::sink($file)
    ->withOptions([
        'allow_redirects' => [
            'max' => 5,
            'on_redirect' => static function (
                RequestInterface $request,
                ResponseInterface $response,
                UriInterface $uri,
            ) use ($network): void {
                if (!$network->isSafeUrl((string) $uri)) {   // same guard on redirects -> same bypass
                    throw UnsafeUrlException::forUrl((string) $uri);
                }
            },
        ],
    ])
    ->get($url)                              // full-read SSRF: response streamed into $file
    ->throw();

$episode->path is the <enclosure url> from the subscribed podcast RSS feed. The redirect callback reuses the same isSafeUrl(), so a redirect to a NAT64/6to4 host is also accepted.

Attack scenario / How input…

Record
GHSA-rjg7-r26h-cfp2
CVSS 4.0
5.3 MediumCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Weakness
CWE-918
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
phanan/koel
Packagist
through 9.7.0
9.7.1
phanan/koel
Packagist
before 9.7.1, plus listed versions
1.0.0-betav0.0.0-betav1.1.1v1.1.2v2.0.0
Show 45 moreShow less
v2.0.1v2.0.2v2.1.0v2.2.0v2.2.1v3.0.0v3.0.1v3.1.0v3.1.1v3.2.0v3.3.0v3.3.1v3.4.0v3.4.1v3.5.0v3.5.1v3.5.2v3.5.3v3.5.4v3.5.5v3.6.0v3.6.1v3.6.2v3.7.0v3.7.1v3.7.2v4.0.0v4.1.0v4.1.1v4.2.0v4.2.1v4.2.2v4.3.0v4.3.1v4.4.0v5.0.0v5.0.1v5.0.2v5.1.0v5.1.1v5.1.10v5.1.11v5.1.12v5.1.13v5.1.14
9.7.1

History

History
RecordedChangedSources
first indexedCVE List, EUVD, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-54494 is a medium vulnerability (CVSS 5.3) in Koel, published July 15, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.43%, the 37th percentile. Fixed in phanan/koel 9.7.1.

References

This page doesn't list everything

Left out here: 120 versions and the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open