Open a private security advisory.
Not a public issue. Issues is where
bugs.url points and is the right place for everything else, but it is a public
tracker, and a report there is a disclosure whether or not it was meant as one.
Expect an acknowledgement within a few days. This is a single-maintainer project with no security team behind it and no bounty; that is worth knowing before you spend a week on it. There is no PGP key — the advisory channel is private end to end, which is what the key would have been for.
Useful in a report, roughly in order: what an attacker ends up able to do, the smallest sequence that gets them there, and which of the two transports it applies to. A proof of concept is welcome and not required.
In scope is anything that breaks a claim the threat model makes. Concretely:
- Recovering file contents, or the session keys, without the QR code.
- Standing between two peers without both screens showing different emoji — that is what the SAS is for, and it failing is the worst bug this project could have.
- Making the receiver accept a file that was truncated, reordered, or altered.
- Decrypting a past transfer given the code afterwards. Each session uses
ephemeral ECDH precisely so this fails. The keypair being genuinely fresh per
pairing is enforced by
test/room.test.mjs, which pairs twice over one secret and checks the sessions cannot open each other; a way to make two pairings share key material is a finding. - Reading anything but ciphertext, timing, and volume as a relay or TURN operator.
- Anything in
src/core/reaching a DOM or anfs. It is enforced by two typecheck passes rather than by review, and a way around that is a finding. - The usual web surface: XSS through a peer-supplied filename, CSP bypass, a way to get the fragment-carried key into a request.
These are documented behaviour. Reporting them is welcome, but the answer will be a link back here.
Anyone holding the code can join. The 32 bytes in the QR are the entire credential — there is no second factor and no account. Showing the code to a room means anyone in the room can pair. This is the design, and it is what makes "no signup" possible. How long a code stays useful — pairing latches on first arrival, there is no TTL, and the window is the sender's unpaired tab lifetime — is set out in the threat model, which is the one home for that reasoning.
Beam is unencrypted, and cannot be encrypted. Animated-QR transfers have no handshake, therefore no key agreement, no forward secrecy, and no SAS: there is no peer to authenticate, only photons, and a key shown on the same screen as the data protects nothing. Anyone who can see the sending screen — including a photograph of it, or a camera in the room — has the file. The UI says so in full before either side commits, tags both beam screens “Not encrypted”, and the threat model carves it out explicitly. Beam exists for air-gapped machines, where the alternative is a USB stick.
Both peers learn each other's IP address. Inherent to a direct connection.
Forcing everything through TURN would hide the peers from each other and expose
both to the relay operator instead; it is left opt-in
(iceTransportPolicy: 'relay').
Relays and trackers see metadata — that two throwaway keys met on a room, when, and roughly how much moved.
The host serving the page could serve modified code. True of every web
application, and not solvable in a browser. It is mitigated by a strict CSP, no
inline scripts, a small surface, and shipped source maps so the deployed bundle
can be read against this repository. The CLI sidesteps it: a versioned tarball
you can pin and audit, published with npm publish --provenance so it is tied
to the workflow run and commit that built it. For genuinely sensitive material,
use the CLI.
The latest published version. This is a young project; there are no backports.