Skip to content

Remove the unauthenticated read_media p2p request - #325

Draft
arsfeld wants to merge 2 commits into
masterfrom
fix/remove-read-media-p2p-request
Draft

Remove the unauthenticated read_media p2p request#325
arsfeld wants to merge 2 commits into
masterfrom
fix/remove-read-media-p2p-request

Conversation

@arsfeld

@arsfeld arsfeld commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Closes an unauthenticated arbitrary file read on the p2p transport.

The problem

read_media accepted a peer-supplied file_path, checked File.exists?, and streamed the bytes back. ReadMediaRequest carried no auth_token field at all, unlike GraphQLRequest and HlsRequest which both do, so nothing in the path checked a credential. The accept loop validates ALPN and nothing else, and iroh publishes the node ID over DNS discovery, so reachability was not obscure. Any peer able to dial the node could read any file the server process could read.

The existing code comment said as much:

# SECURITY: In production, verify path is within allowed directories!

Blast radius is bounded to installs that opted into remote access. remote_access_enabled defaults to false (config/config.exs), and Mydia.P2p.Server only starts when that flag is on. Operators running with remote access enabled should upgrade.

Why delete rather than secure it

Nothing calls it. Verified across .rs, .dart, .ex and .exs: the only references were the type definitions, the NIF dispatch arm, and the handler. Git history confirms it was never live in a shipped client, a0799ba8 removed the last client-side caller and replaced it with notImplemented.

Offline download, the one feature that would plausibly want raw file transfer over p2p, already runs on a safer path: the player requests a job id over authenticated GraphQL, then fetches via an HLS request carrying an auth token with session_id: "download:{jobId}", and the server resolves the path from the job rather than accepting one from the client. That is the capability-lookup pattern, strictly better than confining a client-supplied path. Securing read_media would have meant maintaining a second, weaker way to do what already works.

Changes

Two commits, split so the tree stays both compilable and loadable in between:

  1. Remove the request path: CBOR enum variant, ReadMediaRequest, ElixirReadMediaRequest, the NIF dispatch arm, Mydia.P2p.ReadMediaRequest, and the GenServer handler.
  2. Remove the orphaned respond_with_file_chunk NIF and its Elixir stub, plus the two std imports it was the last user of. These two must land together: the BEAM refuses to load a native library registering a NIF with no matching Elixir function, which fails at load rather than at compile.

Compatibility

No window to manage, which matters because this is self-hosted with no coordinated deploy order between server and player.

serde_cbor 0.11 with a plain derive uses externally tagged enums keyed by variant name ({"ReadMedia": {...}}), not by ordinal. Confirmed against the vendored crate source: Serializer::new defaults to packed: false, and serialize_newtype_variant writes a one-entry map keyed by the variant name. Removing this variant therefore leaves Ping, Pairing, GraphQL, HlsStream and Custom byte-identical on the wire. The fix is server-side only; no client change is needed and none helps.

Verification

  • ./dev mix precommit passed (compile, unused deps, format, credo --strict, tests).
  • Full suite: 6213 tests, 0 failures, 34 skipped (142 excluded).
  • NIF registration confirmed at runtime with the app booted: module loads, function_exported?(Mydia.P2p, :respond_with_file_chunk, 5) is false.
  • All 10 remaining #[rustler::nif] functions audited 1:1 against their Elixir stubs; no orphan in either direction.

Deliberately out of scope

Three p2p authorization gaps remain, all pre-existing and none a regression from this PR:

  • The accept loop still admits any dialer after the ALPN check (no pairing gate).
  • handle_download_stream/4 streams job.output_path with no check that the job belongs to the authenticated user. Requires a valid token, and installs are typically single-household.
  • MydiaRequest::Custom is reachable without a credential. It reaches no handler and discloses nothing, but each request holds a pending-response entry, a task, and an open QUIC stream for the 30 second timeout.

All three are tracked for a follow-up focused on connection-layer authorization.

arsfeld added 2 commits August 3, 2026 23:01
read_media took a peer-supplied file_path, checked File.exists?, and streamed the
bytes back. ReadMediaRequest carries no auth_token, unlike GraphQLRequest and
HlsRequest, so nothing in the path checked a credential. The accept loop
validates ALPN only and iroh publishes the node ID over DNS discovery, so any
peer that could dial the node could read any file the server process could read.
The code comment said as much.

Nothing calls it. Offline download, the one feature that would want raw file
transfer, already runs over an authenticated HLS request that identifies content
by job id and resolves the path server-side, which is strictly safer than
confining a client-supplied one.

serde_cbor tags enum variants by name rather than ordinal, so removing this
variant leaves Ping, Pairing, GraphQL, HlsStream and Custom byte-identical on the
wire. An older client sending one now gets a decode failure instead of a file.
The read_media handler removed in the previous commit was the sole caller of this
NIF. Removing it leaves std::fs::File and std::io::{Read, Seek, SeekFrom}
unused, which would fail the build under warnings-as-errors, so those imports go
too. std::thread stays; another spawn still uses it.

The Rust function and its Elixir stub have to go together, since the BEAM refuses
to load a native library registering a NIF with no matching Elixir function.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant