Skip to content

SSRF via indexer-supplied torrent download URL fetched without scheme or host validation #434

Description

@forkwright

Finding

TorznabClient::download checks only for a magnet: prefix before fetching the URL with the configured HTTP client. Any other URL — http://169.254.169.254/latest/meta-data/ (cloud metadata), http://localhost:6379/ (Redis), or any LAN-internal service — is fetched unconditionally. The url originates from SearchResult.download_url, which is populated from item.link in the indexer-supplied XML, a field a malicious or compromised indexer fully controls. NewznabClient::download has no scheme guard at all, not even the magnet check.

Evidence

crates/zetesis/src/client/torznab.rs:191 fetches any non-magnet URL with no further validation:

if url.starts_with("magnet:") {
    return Ok(DownloadResponse::MagnetUri(url.to_string()));
}
let body = self.fetch_xml(url, ct).await?;

crates/zetesis/src/client/torznab.rs:110 shows the URL is attacker-controlled XML:

let download_url = item.link.unwrap_or_default();

crates/zetesis/src/client/newznab.rs:182 (NewznabClient::download) fetches with no scheme guard whatsoever.

Why this matters

A malicious indexer response drives the Harmonia process into HTTP requests against arbitrary internal addresses. In cloud environments this reaches instance-metadata APIs that vend credentials; on the LAN it reaches NAS management UIs, Synology, or other services that trust intra-network callers without authentication. The response body is recoverable to the attacker through the Torznab parse-error path, which surfaces the fetched body in error messages — turning blind SSRF into a data-exfiltration primitive.

Desired correction

Before calling fetch_xml in download(), validate that the URL scheme is http or https, and reject hosts that resolve to loopback, link-local, or private address ranges (mandatory under the LAN counter-surveillance threat model). Apply the same guard to NewznabClient::download. Done when: download() invoked with http://169.254.169.254/ or http://localhost/ returns an error without issuing any network request, and the guard is shared by both the Torznab and Newznab clients.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions