Skip to main content

Koel EUVDEUVD-2026-63088

| CVE-2026-54491 HIGH
Server-Side Request Forgery (SSRF) (CWE-918)
2026-07-15 https://github.com/koel/koel GHSA-6qvr-wjmv-v8mm
7.1
CVSS 3.1 · Vendor: https://github.com/koel/koel
Share

Severity by source

Vendor (https://github.com/koel/koel) PRIMARY
7.1 HIGH
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N
vuln.today AI
7.1 HIGH

Network-reachable redirect SSRF needing only a low-privilege account (PR:L, AC:L); high confidentiality from reflected IMDSv1 credentials, minor integrity, no availability impact.

3.1 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N
4.0 AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N

Primary rating from Vendor (https://github.com/koel/koel).

CVSS VectorVendor: https://github.com/koel/koel

Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
Low
Availability
None

Lifecycle Timeline

3
Source Code Evidence Fetched
Jul 15, 2026 - 18:34 vuln.today
Analysis Generated
Jul 15, 2026 - 18:34 vuln.today
CVE Published
Jul 15, 2026 - 17:59 github-advisory
HIGH 7.1

DescriptionCVE.org

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.

AnalysisAI

Server-side request forgery in Koel (self-hosted PHP music-streaming server, all releases up to and including 9.7.0) lets any authenticated low-privilege user coerce the server into requesting arbitrary internal or cloud-metadata endpoints. The flaw is an incomplete fix for CVE-2026-47260: the initial isSafeUrl() check was added to several podcast/radio fetchers, but per-redirect-hop re-validation was wired into only one path (EpisodePlayable), leaving every sibling fetcher bypassable via an attacker-controlled host that returns an HTTP 302 to an internal address, and all paths bypassable via DNS rebinding. A researcher mechanism PoC confirms exploitation; not listed in CISA KEV and no evidence of active exploitation.

Technical ContextAI

The vulnerability is CWE-918 (SSRF) in a Laravel/PHP application that fetches remote podcast feeds and internet-radio stream URLs on behalf of users via the Guzzle HTTP client and Laravel's Http facade. The root cause is that App\Helpers\Network::isPublicHost()/isSafeUrl() perform a point-in-time host check with no pinning of the resolved IP, and the compensating per-hop on_redirect callback (introduced in follow-up commit be1e867) was applied only in App\Values\Podcast\EpisodePlayable. The remaining fetchers - Poddle::fromUrl() (used by PodcastService::addPodcast/refreshPodcast), PodcastService::getStreamableUrl() (uses ALLOW_REDIRECTS with track_redirects but no on_redirect), PodcastService::isPodcastObsolete() (Http::head with no check at all), the HasAudioContentType rule (extending exposure to internet-radio), and the SafeUrl rule (validates only the final effective host) - either lack per-hop validation or lack any check. Two distinct primitives result: redirect-SSRF (initial URL passes, then a cross-host 302 to an internal target is followed unvalidated) and DNS-rebinding TOCTOU (validator and connect-time resolver look up DNS separately with no IP pinning). Affected package is pkg:composer/phanan_koel.

RemediationAI

Vendor-released patch: upgrade to Koel 9.7.1 or later, which is the primary and recommended fix (advisory https://github.com/koel/koel/security/advisories/GHSA-6qvr-wjmv-v8mm; fixes in PRs https://github.com/koel/koel/pull/2546 and https://github.com/koel/koel/pull/2549). The fix centralizes outbound HTTP through a shared App\Helpers\SafeHttp handler that re-validates every redirect hop against the SafeUrl rules and injects a SafeHttp-backed PSR-18 client for libraries such as Poddle; note the vendor explicitly documents that this closes the redirect-SSRF leg but that DNS-rebinding TOCTOU (needing connect-time IP pinning, e.g. CURLOPT_RESOLVE) is tracked separately, so upgrading alone does not fully eliminate the rebinding vector. If you cannot upgrade immediately, compensating controls include: enforce IMDSv2 and disable IMDSv1 on cloud instances so metadata credentials are not reachable via GET-redirect SSRF (low side-effect, high value on AWS); block the Koel server's egress to link-local 169.254.169.254 and RFC1918/loopback ranges at the host or network firewall (may break legitimate internal integrations if any); and restrict who can create podcasts/radio stations since PodcastController::store lacks an authorization check (trade-off: limits podcast/radio functionality for non-trusted users). Avoid per-call-site patches - the vendor guidance is to route all outbound HTTP through one validating, IP-pinning handler.

More in PHP

View all
CVE-2019-11043 CRITICAL POC
9.8 Oct 28

In PHP versions 7.1.x below 7.1.33, 7.2.x below 7.2.24 and 7.3.x below 7.3.11 in certain configurations of FPM setup it

CVE-2012-1823 CRITICAL POC
9.8 May 11

sapi/cgi/cgi_main.c in PHP before 5.3.12 and 5.4.x before 5.4.2, when configured as a CGI script (aka php-cgi), does not

CVE-2016-1555 CRITICAL POC
9.8 Apr 21

(1) boardData102.php, (2) boardData103.php, (3) boardDataJP.php, (4) boardDataNA.php, and (5) boardDataWW.php in Netgear

CVE-2018-11138 CRITICAL POC
9.8 May 31

The '/common/download_agent_installer.php' script in the Quest KACE System Management Appliance 8.0.318 is accessible by

CVE-2024-11680 CRITICAL POC
9.8 Nov 26

ProjectSend versions prior to r1720 are affected by an improper authentication vulnerability. Rated critical severity (C

CVE-2025-49113 CRITICAL POC
9.9 Jun 02

Roundcube Webmail contains a critical PHP object deserialization vulnerability (CVE-2025-49113, CVSS 9.9) that allows au

CVE-2017-9841 CRITICAL POC
9.8 Jun 27

Util/PHP/eval-stdin.php in PHPUnit before 4.8.28 and 5.x before 5.6.3 allows remote attackers to execute arbitrary PHP c

CVE-2025-0108 HIGH POC
8.8 Feb 12

Palo Alto Networks PAN-OS management web interface contains an authentication bypass allowing unauthenticated attackers

CVE-2021-25298 HIGH POC
8.8 Feb 15

Nagios XI version xi-5.7.5 is affected by OS command injection. Rated high severity (CVSS 8.8), this vulnerability is re

CVE-2021-25296 HIGH POC
8.8 Feb 15

Nagios XI version xi-5.7.5 is affected by OS command injection. Rated high severity (CVSS 8.8), this vulnerability is re

CVE-2013-4983 CRITICAL POC
10.0 Sep 10

The get_referers function in /opt/ws/bin/sblistpack in Sophos Web Appliance before 3.7.9.1 and 3.8 before 3.8.1.1 allows

CVE-2023-6553 CRITICAL POC
9.8 Dec 15

The Backup Migration plugin for WordPress is vulnerable to Remote Code Execution in all versions up to, and including, 1

Share

EUVD-2026-63088 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy