Mend.io Vulnerability Database
The largest open source vulnerability database
What is a Vulnerability ID?
New vulnerability? Tell us about it!
CVE-2026-54491
Published:July 15, 2026
Updated:August 02, 2026
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, attacker_router.php, internal_router.php) is available on request. End-to-end on a real instance: an authenticated user "POST /api/podcasts" (or Subsonic "createPodcastChannel") with a feed URL on an attacker host that returns "302 Location: http://169.254.169.254/latest/meta-data/..." (or "http://127.0.0.1:<port>/"); the server follows it. The response is reflected back via parsed podcast fields / "getStreamableUrl" when the internal endpoint returns "Access-Control-Allow-Origin: *"; otherwise blind SSRF via status/timing. Impact Authenticated (any user) SSRF: access to cloud instance metadata (IAM credentials on IMDSv1), internal-only admin panels, and internal network service probing — from the Koel server's network position. Same threat model as CVE-2026-47260. Attack scenario (fully remote, no user interaction): the only precondition is a single low-privilege account. On AWS/GCP/Azure-hosted instances, redirecting to the metadata IP and reflecting the body discloses temporary IAM credentials → cloud-account pivot. (AWS IMDSv2's token-via-PUT is not reachable through a simple GET-redirect SSRF; IMDSv1 instances are fully exposed.) "Koel only runs on an internal/trusted network" does not reduce the risk — the bug makes the Koel server itself the attacker's pivot into that trusted network and cloud control plane. Suggested severity: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N (7.1); lower where exploitation is blind. Remediation Do not fix per-call-site. Centralize: route all outbound HTTP through a shared Guzzle handler/middleware that, on every connection and every redirect hop, resolves the target and rejects private/reserved IPs, and pins the validated IP for the actual connection (defeats DNS rebinding). Apply to "EpisodePlayable", "getStreamableUrl", "Poddle::fromUrl" usage, "isPodcastObsolete", "HasAudioContentType", and the "SafeUrl" rule.
Affected Packages
https://github.com/koel/koel.git (GITHUB):
Affected version(s) >=v9.3.0 <v9.7.1
Fix Suggestion:
Update to version v9.7.1
phanan/koel (PHP):
Affected version(s) >=dev-release <v9.7.1
Fix Suggestion:
Update to version v9.7.1
Do you need more information?
Contact Us
CVSS v4
Base Score:
7.1
Attack Vector
NETWORK
Attack Complexity
LOW
Attack Requirements
NONE
Privileges Required
LOW
User Interaction
NONE
Vulnerable System Confidentiality
HIGH
Vulnerable System Integrity
LOW
Vulnerable System Availability
NONE
Subsequent System Confidentiality
NONE
Subsequent System Integrity
NONE
Subsequent System Availability
NONE
CVSS v3
Base Score:
7.1
Attack Vector
NETWORK
Attack Complexity
LOW
Privileges Required
LOW
User Interaction
NONE
Scope
UNCHANGED
Confidentiality
HIGH
Integrity
LOW
Availability
NONE
Weakness Type (CWE)
Server-Side Request Forgery (SSRF)