Every CVE Search/ Get API key

CVE-2026-54491

Koel: Incomplete fix for CVE-2026-47260 — systemic SSRF in podcast & radio fetch paths

7.1 HighFix available

Koel is a free, open-source music streaming solution. Prior to 9.7.1, outbound podcast and radio fetch paths perform a point-in-time App\Helpers\Network::isPublicHost() or isSafeUrl() check without pinning the validated address, and most paths lack redirect-hop validation and do not revalidate every redirect target. PhanAn\Poddle\Poddle::fromUrl(), PodcastService::getStreamableUrl(), PodcastService::isPodcastObsolete(), App\Rules\HasAudioContentType, and App\Rules\SafeUrl can therefore follow an attacker-controlled redirect to an internal address or connect after DNS rebinding changes a public resolution to a private one. These paths are reachable through podcast and radio APIs, including createPodcastChannel, createInternetRadioStation, refreshPodcasts, apiResource podcasts, and radio/stations, allowing an authenticated user to request internal services or cloud metadata and potentially receive parsed or streamed response content. 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
High
Integrity
Low
Availability
None

Exploit likelihood

0.38%Up 0.1 points since Aug 20, 2026
90%50%10%1%

What each source reports

CVE List Score used 7.1

Koel: Incomplete fix for CVE-2026-47260 — systemic SSRF in podcast & radio fetch paths

Koel is a free, open-source music streaming solution. Prior to 9.7.1, outbound podcast and radio fetch paths perform a point-in-time App\Helpers\Network::isPublicHost() or isSafeUrl() check without pinning the validated address, and most paths lack redirect-hop validation and do not revalidate every redirect target. PhanAn\Poddle\Poddle::fromUrl(), PodcastService::getStreamableUrl(), PodcastService::isPodcastObsolete(), App\Rules\HasAudioContentType, and App\Rules\SafeUrl can therefore follow an attacker-controlled redirect to an internal address or connect after DNS rebinding changes a public resolution to a private one. These paths are reachable through podcast and radio APIs, including createPodcastChannel, createInternetRadioStation, refreshPodcasts, apiResource podcasts, and radio/stations, allowing an authenticated user to request internal services or cloud metadata and potentially receive parsed or streamed response content. This issue is fixed in version 9.7.1.

Record
CVE-2026-54491
CVSS 3.1
7.1 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N
Weakness
CWE-918
Updated
2026-11-02
EUVDEUVD-2026-63088 7.1

Koel is a free, open-source music streaming solution. Prior to 9.7.1, outbound podcast and radio fetch paths perform a point-in-time App\Helpers\Network::isPublicHost() or isSafeUrl() check without pinning the validated address, and most paths lack redirect-hop validation and do not revalidate every redirect target. PhanAn\Poddle\Poddle::fromUrl(), PodcastService::getStreamableUrl(), PodcastService::isPodcastObsolete(), App\Rules\HasAudioContentType, and App\Rules\SafeUrl can therefore follow an attacker-controlled redirect to an internal address or connect after DNS rebinding changes a public resolution to a private one. These paths are reachable through podcast and radio APIs, including createPodcastChannel, createInternetRadioStation, refreshPodcasts, apiResource podcasts, and radio/stations, allowing an authenticated user to request internal services or cloud metadata and potentially receive parsed or streamed response content. This issue is fixed in version 9.7.1.

Record
EUVD-2026-63088
CVSS 3.1
7.1 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N
Updated
2026-11-02
GitHub AdvisoriesGHSA-6qvr-wjmv-v8mm 7.1

Koel: Incomplete fix for CVE-2026-47260 — systemic SSRF in podcast & radio fetch paths

Summary

The fix for CVE-2026-47260 (v9.3.5) added an initial isSafeUrl() check to several fetchers (synchronizeEpisodes, getStreamableUrl, AddRadioStation, EpisodePlayable), but the redirect-target validation — the per-hop Guzzle on_redirect callback added in follow-up commit be1e867 — was applied to only one path, EpisodePlayable. Every other server-side fetcher therefore has only the initial check, which an HTTP 302 redirect to an internal address bypasses, or no check at all. DNS rebinding (validation and connection resolve DNS separately, with no IP pinning) bypasses the initial check on every path.

An authenticated, non-admin user can thus cause the Koel server to issue requests to arbitrary internal / cloud-metadata endpoints (SSRF) by supplying a URL on an attacker-controlled host that 302-redirects to an internal address.

Note: commit be1e867 shows the redirect-based SSRF vector was recognised, but the redirect defense was applied to a single call site rather than generalised — so the class survives in the sibling paths below.

Details — Root cause

App\Helpers\Network::isPublicHost() / isSafeUrl() perform a point-in-time host check with no pinning of the resolved IP, and per-redirect-hop re-validation exists only in App\Values\Podcast\EpisodePlayable (the on_redirect callback from commit be1e867). Consequently every other fetcher is exposed to (1) redirect SSRF — initial URL passes isSafeUrl, then the HTTP client follows a cross-host 302 to an internal target without re-validating the hop; and (2) DNS rebinding (TOCTOU) — isPublicHost resolves DNS at validation, the HTTP client resolves again at connect time.

Affected paths (all reachable by any authenticated user)

| # | Location | Issue | |---|----------|-------| | 1 | PhanAn\Poddle\Poddle::fromUrl() → Http::timeout()->get($url) (used by PodcastService::addPodcast/refreshPodcast) | Plain Http::get, follows redirects, no per-hop validation; refreshPodcast does not re-run isSafeUrl at all | | 2 | PodcastService::getStreamableUrl() (PodcastService.php:244/251) | Has the initial isSafeUrl() (line 244) but the request uses ALLOW_REDIRECTS => ['track_redirects' => true] with no on_redirect → 302 to internal is followed. Called at episode stream time via PodcastStreamerAdapter. Also DNS-rebinding-exposed | | 3 | PodcastService::isPodcastObsolete() (:221) Http::head($podcast->url) | No isSafeUrl, no redirect validation | | 4 | App\Rules\HasAudioContentType (:45/:54) Http::head/Http::get | Self-documented "use after SafeUrl"; ordering-dependent, no own validation, no per-hop check. Extends the surface to the internet-radio feature (RadioStationStore/UpdateRequest) | | 5 | App\Rules\SafeUrl validator (:52/:56) | Follows redirects, validates only the final effective host — intermediate-hop requests still fire |

Reachable via the native API (apiResource podcasts, radio/stations; PodcastController::store has no authorization check, only #[DisabledInDemo]) and the Subsonic API (createPodcastChannel, createInternetRadioStation, refreshPodcasts).

PoC

A mechanism PoC that runs the exact Guzzle/Laravel-Http call shapes Koel uses (attacker-redirect server + internal-target listener on loopback), verified on PHP 8.2 + Guzzle 7:

isPublicHost('127.0.0.1') = false        # a per-hop check WOULD block this
Case1 Poddle::fromUrl        -> [VULNERABLE]  leaked INTERNAL-SECRET-TOKEN
Case2 getStreamableUrl       -> [VULNERABLE]  leaked INTERNAL-SECRET-TOKEN
Case3 EpisodePlayable        -> [BLOCKED]     UnsafeUrl on redirect
internal_hits.log: 2 hits    # internal service actually reached by Case1 + Case2

Case1/Case2 reaching the internal target while Case3 (the fixed path) blocks under identical conditions demonstrates the incomplete remediation. The full PoC kit (poc.php,…

Record
GHSA-6qvr-wjmv-v8mm
CVSS 3.1
7.1 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N
Weakness
CWE-918
Updated
2026-11-02
NVD 7.1

Koel is a free, open-source music streaming solution. Prior to 9.7.1, outbound podcast and radio fetch paths perform a point-in-time App\Helpers\Network::isPublicHost() or isSafeUrl() check without pinning the validated address, and most paths lack redirect-hop validation and do not revalidate every redirect target. PhanAn\Poddle\Poddle::fromUrl(), PodcastService::getStreamableUrl(), PodcastService::isPodcastObsolete(), App\Rules\HasAudioContentType, and App\Rules\SafeUrl can therefore follow an attacker-controlled redirect to an internal address or connect after DNS rebinding changes a public resolution to a private one. These paths are reachable through podcast and radio APIs, including createPodcastChannel, createInternetRadioStation, refreshPodcasts, apiResource podcasts, and radio/stations, allowing an authenticated user to request internal services or cloud metadata and potentially receive parsed or streamed response content. This issue is fixed in version 9.7.1.

Record
CVE-2026-54491
CVSS 3.1
7.1 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N
Weakness
CWE-918
Updated
2026-11-02
OSV 7.1

Koel: Incomplete fix for CVE-2026-47260 — systemic SSRF in podcast & radio fetch paths

Koel is a free, open-source music streaming solution. Prior to 9.7.1, outbound podcast and radio fetch paths perform a point-in-time App\Helpers\Network::isPublicHost() or isSafeUrl() check without pinning the validated address, and most paths lack redirect-hop validation and do not revalidate every redirect target. PhanAn\Poddle\Poddle::fromUrl(), PodcastService::getStreamableUrl(), PodcastService::isPodcastObsolete(), App\Rules\HasAudioContentType, and App\Rules\SafeUrl can therefore follow an attacker-controlled redirect to an internal address or connect after DNS rebinding changes a public resolution to a private one. These paths are reachable through podcast and radio APIs, including createPodcastChannel, createInternetRadioStation, refreshPodcasts, apiResource podcasts, and radio/stations, allowing an authenticated user to request internal services or cloud metadata and potentially receive parsed or streamed response content. This issue is fixed in version 9.7.1.

Record
CVE-2026-54491
CVSS 3.1
7.1 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N
Weakness
CWE-918
Updated
2026-11-02
OSVGHSA-6qvr-wjmv-v8mm 7.1

Koel: Incomplete fix for CVE-2026-47260 — systemic SSRF in podcast & radio fetch paths

Summary

The fix for CVE-2026-47260 (v9.3.5) added an initial isSafeUrl() check to several fetchers (synchronizeEpisodes, getStreamableUrl, AddRadioStation, EpisodePlayable), but the redirect-target validation — the per-hop Guzzle on_redirect callback added in follow-up commit be1e867 — was applied to only one path, EpisodePlayable. Every other server-side fetcher therefore has only the initial check, which an HTTP 302 redirect to an internal address bypasses, or no check at all. DNS rebinding (validation and connection resolve DNS separately, with no IP pinning) bypasses the initial check on every path.

An authenticated, non-admin user can thus cause the Koel server to issue requests to arbitrary internal / cloud-metadata endpoints (SSRF) by supplying a URL on an attacker-controlled host that 302-redirects to an internal address.

Note: commit be1e867 shows the redirect-based SSRF vector was recognised, but the redirect defense was applied to a single call site rather than generalised — so the class survives in the sibling paths below.

Details — Root cause

App\Helpers\Network::isPublicHost() / isSafeUrl() perform a point-in-time host check with no pinning of the resolved IP, and per-redirect-hop re-validation exists only in App\Values\Podcast\EpisodePlayable (the on_redirect callback from commit be1e867). Consequently every other fetcher is exposed to (1) redirect SSRF — initial URL passes isSafeUrl, then the HTTP client follows a cross-host 302 to an internal target without re-validating the hop; and (2) DNS rebinding (TOCTOU) — isPublicHost resolves DNS at validation, the HTTP client resolves again at connect time.

Affected paths (all reachable by any authenticated user)

| # | Location | Issue | |---|----------|-------| | 1 | PhanAn\Poddle\Poddle::fromUrl() → Http::timeout()->get($url) (used by PodcastService::addPodcast/refreshPodcast) | Plain Http::get, follows redirects, no per-hop validation; refreshPodcast does not re-run isSafeUrl at all | | 2 | PodcastService::getStreamableUrl() (PodcastService.php:244/251) | Has the initial isSafeUrl() (line 244) but the request uses ALLOW_REDIRECTS => ['track_redirects' => true] with no on_redirect → 302 to internal is followed. Called at episode stream time via PodcastStreamerAdapter. Also DNS-rebinding-exposed | | 3 | PodcastService::isPodcastObsolete() (:221) Http::head($podcast->url) | No isSafeUrl, no redirect validation | | 4 | App\Rules\HasAudioContentType (:45/:54) Http::head/Http::get | Self-documented "use after SafeUrl"; ordering-dependent, no own validation, no per-hop check. Extends the surface to the internet-radio feature (RadioStationStore/UpdateRequest) | | 5 | App\Rules\SafeUrl validator (:52/:56) | Follows redirects, validates only the final effective host — intermediate-hop requests still fire |

Reachable via the native API (apiResource podcasts, radio/stations; PodcastController::store has no authorization check, only #[DisabledInDemo]) and the Subsonic API (createPodcastChannel, createInternetRadioStation, refreshPodcasts).

PoC

A mechanism PoC that runs the exact Guzzle/Laravel-Http call shapes Koel uses (attacker-redirect server + internal-target listener on loopback), verified on PHP 8.2 + Guzzle 7:

isPublicHost('127.0.0.1') = false        # a per-hop check WOULD block this
Case1 Poddle::fromUrl        -> [VULNERABLE]  leaked INTERNAL-SECRET-TOKEN
Case2 getStreamableUrl       -> [VULNERABLE]  leaked INTERNAL-SECRET-TOKEN
Case3 EpisodePlayable        -> [BLOCKED]     UnsafeUrl on redirect
internal_hits.log: 2 hits    # internal service actually reached by Case1 + Case2

Case1/Case2 reaching the internal target while Case3 (the fixed path) blocks under identical conditions demonstrates the incomplete remediation. The full PoC kit (poc.php,…

Record
GHSA-6qvr-wjmv-v8mm
CVSS 3.1
7.1 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A: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-54491 is a high vulnerability (CVSS 7.1) 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.38%, the 32nd percentile. Fixed in phanan/koel 9.7.1.

References

Show 4 moreShow less
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