Skip to content

Security: peerloomllc/peartune

Security

SECURITY.md

Security

PearTune plays music from a computer you own, to devices you have let in. That means a program on your machine accepts connections from the internet, reads your files and runs ffmpeg on them. This page says what protects what, so you do not have to read the code to find out.

Reporting a problem

Email peerloomllc@proton.me. If it is a real vulnerability, please give us a chance to ship a fix before you publish it. There is no bounty. There is a thank you in the release notes if you want one.

MIT licensed and developed in the open, so anything claimed here can be checked: https://github.com/peerloomllc/peartune

What an attacker would be trying to reach

The host is the interesting target. It runs on the machine holding your music, it is reachable over the internet by design, and it can read your library. The phone app matters less: it holds no library and no keys worth stealing beyond its own.

The boundaries that hold this up

A stranger cannot make the host do anything

The host is found by key, not by an address, and every connection is encrypted and authenticated by the Noise handshake before a single byte of ours is read. An unknown key is refused at the firewall (gate.js in @peerloom/host), and refused again when it tries to open the media channel, because the first refusal has a deliberate exemption while a pairing window is open and the second does not. A connection without a grant gets no method table at all, so there is nothing to call.

If an error is thrown while deciding, the connection is denied. HyperDHT initialises its hook as firewalled: true and swallows a throw, and test/gate.test.js pins that behaviour so a future dependency bump cannot silently flip it open.

There are no passwords, tokens or connection strings

Noise tells the host which device is calling, so there is nothing to steal, leak or forget to rotate. This is why we did not build on a tunnel: a tunnel hands a guest the whole of whatever is behind it, plus its credentials. See DECISIONS.md 2026-07-13.

The allow-list is host-local and never replicated

Who may connect lives in a store only the host writes and nothing syncs. If it were in a shared ledger, a revoked device would still hold a writer key and could append itself back onto the list. The host is the sole authority on admission.

Revoking cuts a device off now, not at its next login

The firewall only runs at connect time, so the host also tracks every live connection per device and destroys them when you revoke. Within a second: no reconnect, no browse, no next track, no artwork. Music already buffered on the phone may finish playing - that is deliberate, so switching between wifi and mobile data does not stop the music - and nothing new is ever served. The revoked device is also told once why it was refused, so it stops knocking and can say so plainly instead of blaming your network.

One person can be narrowed to chosen folders

A person can be given part of a library rather than all of it. Everything else is not listed, not searchable, not streamable and has no artwork for them; a group (an album, an artist) is visible only if at least one of its tracks is. Two rules make it safe rather than approximate: anything the host cannot place on disk is hidden from a narrowed person, and a music source that cannot enforce the rule at all serves them nothing rather than everything.

ffmpeg is never handed to a shell

Every ffmpeg and ffprobe run is spawn(binary, [arguments]), with the arguments as separate array entries (host/transcode.js). There is no shell: true, no exec, and no command assembled by string concatenation anywhere in the host.

A phone cannot ask for a file by path

Track, album and artist ids are namespaced hashes, not paths (protocol/ids.js). They are not invertible and cannot be walked: an id the host does not know is simply not found, so there is no ../.. to attempt. The host's own filesystem layout is never sent to a phone.

A device can only ever speak about itself

Everything a phone writes - its name, its photo, its favourites, its resume points, its requests - is keyed to the identity of the connection it arrived on, never to a parameter the phone supplies. A device cannot name itself as somebody else, and it cannot attach itself to an existing person: only the operator confirms that, on the dashboard.

The dashboard

The dashboard can revoke every device and open a pairing window onto the whole library, so it is treated as a control plane.

  • It refuses to start unauthenticated on a network. A non-loopback bind with no password throws rather than warns (requireSafeBind). On Umbrel and Start9 the platform supplies the password.
  • Failed logins are rate-limited, and the session cookie is SameSite=Strict.
  • When there is no password - the desktop tray app, which binds to loopback only - requests must come from loopback by both Host and Origin. That closes two attacks a bare loopback bind does not: a web page you visit blind-POSTing to the dashboard, and DNS rebinding, where a page re-resolves its own name to 127.0.0.1 so the browser hands it the answers. A password-protected install is untouched by this check, because the cookie already covers both.

The phone app

  • The bundled page is the only thing the WebView loads; any attempt to navigate elsewhere is refused, and an ordinary web link is handed to the browser instead.
  • The shell will open only https:, mailto:, lightning: and bitcoin: URLs. Anything else - notably Android's intent:// and file:// - is refused.
  • The only capture permission granted is the camera, for the pairing QR scanner.
  • The app asks for the network, the camera and (on Android) notifications and foreground playback. It has no account, sends no analytics and phones nothing home.

When a direct connection is impossible

Some mobile networks refuse a direct connection between two devices. PearTune then falls back to a relay we run, which forwards data it cannot read - the encryption is end-to-end between your phone and your host, and the relay holds no keys and keeps no copy. Your phone always tries a direct connection first, asks before streaming a library's music over the relay, and Settings can disable it entirely.

What this does not claim

  • It does not protect a machine somebody already controls. A host with a compromised operating system is compromised.
  • It does not hide that you are running PearTune from someone watching your network, only what you are playing.
  • A device you have let in can see the music you let it see. Sharing is a decision, and revoking is how you undo it.

There aren't any published security advisories