Koel Vulnerable to SSRF via Podcast Episode Enclosure URLs
Date: 2026-05
Affected: composer/phanan/koel
Attack vector: ssrf
Category: vulnerability-disclosure
Severity: High
CVE: CVE-2026-47260
CWE: CWE-918
CVSS: 7.7 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N)

## 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


Mitigations:

Architectural layers (MAESTRO):