set_playlist: unrestricted URL fetch (SSRF) with unbounded response buffering #4
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
A paired client can send
set_playlistwith any URL.SourceRepository.open()(SourceRepository.kt:99-102) creates anHttpURLConnectionwith redirects enabled and no scheme or host allow-list, thenSourceRepository.kt:109reads the whole body withbufferedReader().use { it.readText() }— no size cap.Failure scenarios
Reach into the LAN and read the answer. The client points the TV at an internal address it cannot reach itself (router admin page, an internal service). The response comes back:
M3uParsertreats every non-#line as a channel (Channel(name = line, url = line, …)), and those lines are broadcast verbatim to the client as the channel list. Line-oriented response bodies are exfiltrated straight into the remote UI.Out-of-memory crash.
readTimeoutis a per-read socket deadline, not an overall transfer limit, so a hostile server can trickle bytes forever while theStringgrows unbounded. The 10,000-channel cap in the parser only applies after the full body is already in memory. Any paired client can crash the app with a multi-gigabyte or slow-drip response.Note:
file://is not reachable — the hard cast toHttpURLConnectionthrows for non-HTTP schemes and the exception is swallowed.Suggested fix
http/httpsand reject private/loopback/link-local targets (and re-check after each redirect)Found by multi-agent code review; verified against current
SourceRepository.kt.Fixed in
dd612fc, shipped in v0.9.0.open()now validates the scheme and rejects anything that is nothttp/httpsbefore opening the connection.download()streams into a builder with a hard 24 MB cap and an overall 120 s transfer deadline, so a slow-drip or oversized response fails fast instead of growing the buffer until the app dies.M3uParsernow rejects payloads without playlist markers (#10), so a fetched HTML page or JSON body no longer turns into "channels" that get broadcast back to the client.What remains by design: the target host is not restricted to public addresses. A LAN address is exactly what the generic source is for (ErsatzTV, Threadfin, a local Dispatcharr), and only an already-paired client can call
set_playlist— the pairing path itself is what got hardened in #2.