# Koel: Incomplete fix for CVE-2026-47260 — systemic SSRF in podcast & radio fetch paths

Koel's SSRF fix only re-validated redirect hops in one fetcher; other podcast/radio paths follow 302s to internal or metadata IPs, leaking IAM credentials.

- Payout: Research
- Severity: high
- Program: Koel
- Found by: kiffa-australis256
- Disclosed: 2026-07-15
- CVE: CVE-2026-54491
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-6qvr-wjmv-v8mm
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-6qvr-wjmv-v8mm
- Tags: area:web domain:web impact:data-leak impact:internal-access kind:research platform:ghsa sev:high tech:aws tech:php technique:incomplete-fix technique:source-review vuln:access-control vuln:ssrf

**Target:** Podcast/radio URL fetchers (Poddle::fromUrl, getStreamableUrl, isPodcastObsolete, HasAudioContentType, SafeUrl rule)
**Root cause:** isSafeUrl/isPublicHost do a point-in-time host check with no IP pinning, and per-hop redirect re-validation (on_redirect from commit be1e867) exists only in EpisodePlayable; all sibling fetchers follow cross-host 302s to internal addresses unchecked.
**Steps:**
1. Authenticate as any low-privilege user.
2. Host a feed URL that returns `302 Location: http://169.254.169.254/latest/meta-data/...` (or `http://127.0.0.1:<port>/`).
3. POST /api/podcasts (or Subsonic createPodcastChannel) with that URL; initial isSafeUrl passes on attacker host.
4. Server follows the 302 to the internal target without re-validating the hop.
5. Read leaked data via parsed podcast fields / getStreamableUrl when internal endpoint returns `Access-Control-Allow-Origin: *`, else blind via status/timing.
**How found:** Reviewed the CVE-2026-47260 fix and noticed commit be1e867 added redirect-hop validation to only EpisodePlayable; audited sibling fetchers and found initial-only or absent checks, then built a PoC harness replicating exact Guzzle/Laravel-Http call shapes to confirm Case1/Case2 reach internal target while Case3 blocks.
**Exploitation:** Single authenticated account submits attacker-controlled feed URL to podcast/radio endpoints; Koel server follows redirect to IMDSv1 or internal services, reflecting body through parsed podcast fields or getStreamableUrl, disclosing temporary IAM credentials for cloud pivot.
**Escalation:** Metadata credential theft pivots from Koel host to cloud control plane; internal network probing from server's trusted position.
**Insight:** When a vendor patches SSRF with a per-hop on_redirect callback, enumerate every other outbound HTTP call site in the codebase — fixes applied at one call site rather than a shared handler leave sibling fetchers exploitable by the same 302 trick.

