GHSA-7j2f-6h2r-6cqc: Koel Vulnerable to SSRF via Podcast Episode Enclosure URLs
SummaryKoel 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 DetailsEpisode URL Stored Without Validation**File:** `app/Services/Podcast/PodcastService.php`, line 146The `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 42When 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`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 FlowAttacker registers an account (Community edition, no Plus required)Attacker hosts a malicious RSS feed on a public server:`POST /api/podcasts` with `url=https://evil.com/feed.xml` — passes `SafeUrl` (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 attackerProof of ConceptImpact**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 attackerSecondary Finding: SSRF Bypass via AI Radio Station Tool**File:** `app/Ai/Tools/AddRadioStation.php`, lines 35-38The 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()`:**Fix 2:** Defense-in-depth validation at playback time in `EpisodePlayable::createForEpisode()`.**Fix 3:** Add `SafeUrl` validation in `AddRadioStation` AI tool.
CVSS v3.1: 7.7