Critical: a Tor-routed account's links are fetched from the real IP address #257

Closed
opened 2026-10-02 19:41:03 -04:00 by Salastil · 1 comment
Owner

Turning Tor on for one account does not route what the window fetches for that account's messages. It routes the daemon's connection to the service, and media the daemon fetches itself (Sneedchat avatars and attachments, Matrix media). Everything the window fetches on its own goes out directly, from the user's real address:

  • the media sniffer's HEAD to every link it cannot classify (src/renderer/src/lib/sniff.ts, which says so: "not routed through nobilis or ... its embedded Tor client"), sent the moment a message arrives, clicked or not - and on by default ("Embed unrecognized links by content type");
  • images and video linked in messages, drawn straight from the linked host;
  • YouTube thumbnails, link previews, embed images, remote avatars.

Only the global "send everything through Tor" switch routes the window (src/main/tunnel.ts, via the daemon's netTunnel).

The exploit. In a channel on an IRC network the user reaches over Tor, anyone can post a link to a server they control. moho requests it at once from the user's real IP - tying that address to the nick, the channel and the moment, which is exactly what routing the account was for. The same applies to an outside link in a Sneedchat room, and to any routed account on any service. No click, no interaction: receiving the message is enough.

Fix: media for a routed account goes through that account's route, or not at all

The daemon already has the route the window needs: router.ready() answers with a SOCKS address - moho's own Tor through its loopback relay, or the proxy the user named - and tunnel.ts uses it, but only when everything is tunnelled.

  1. Daemon: an RPC (or netTunnel extended) that answers the SOCKS address for the routed path whenever any account is routed, not only under tunnel-all.
  2. Main process: a separate session (session.fromPartition('moho-routed')) proxied to that address, and a scheme - moho-routed://fetch/?u=<url> - whose handler fetches http(s) URLs through that session and streams the answer back (GET and HEAD, range requests for video, no cookies, timeouts and a size cap). Fails closed: with no route yet, it refuses rather than fetching directly - the same rule tunnel.ts follows.
  3. Window: one helper, remoteUrlFor(accountId, url), through which every remote URL the window would load for a message passes: unchanged for an unrouted account, rewritten to moho-routed:// for a routed one. Used by the sniffer, embeds, YouTube thumbnails, link previews, embed images and avatars - so media still embeds on Tor-routed services, it just arrives over Tor.
  4. Test: a check that while a routed account's messages are on screen, the default session makes no request to any outside host (session.webRequest.onBeforeRequest on the default session, in the layout-check harness against the fake IRC server with the account routed).

Until this lands, the sniffer at least should be off for routed accounts.

**Turning Tor on for one account does not route what the window fetches for that account's messages.** It routes the daemon's connection to the service, and media the daemon fetches itself (Sneedchat avatars and attachments, Matrix media). Everything the window fetches on its own goes out directly, from the user's real address: - the media sniffer's `HEAD` to every link it cannot classify (`src/renderer/src/lib/sniff.ts`, which says so: *"not routed through nobilis or ... its embedded Tor client"*), sent the moment a message arrives, clicked or not - and on by default ("Embed unrecognized links by content type"); - images and video linked in messages, drawn straight from the linked host; - YouTube thumbnails, link previews, embed images, remote avatars. Only the global "send everything through Tor" switch routes the window (`src/main/tunnel.ts`, via the daemon's `netTunnel`). **The exploit.** In a channel on an IRC network the user reaches over Tor, anyone can post a link to a server they control. moho requests it at once from the user's real IP - tying that address to the nick, the channel and the moment, which is exactly what routing the account was for. The same applies to an outside link in a Sneedchat room, and to any routed account on any service. No click, no interaction: receiving the message is enough. ## Fix: media for a routed account goes through that account's route, or not at all The daemon already has the route the window needs: `router.ready()` answers with a SOCKS address - moho's own Tor through its loopback relay, or the proxy the user named - and `tunnel.ts` uses it, but only when everything is tunnelled. 1. **Daemon:** an RPC (or `netTunnel` extended) that answers the SOCKS address for the routed path whenever any account is routed, not only under tunnel-all. 2. **Main process:** a separate session (`session.fromPartition('moho-routed')`) proxied to that address, and a scheme - `moho-routed://fetch/?u=<url>` - whose handler fetches http(s) URLs through that session and streams the answer back (GET and HEAD, range requests for video, no cookies, timeouts and a size cap). **Fails closed**: with no route yet, it refuses rather than fetching directly - the same rule `tunnel.ts` follows. 3. **Window:** one helper, `remoteUrlFor(accountId, url)`, through which every remote URL the window would load for a message passes: unchanged for an unrouted account, rewritten to `moho-routed://` for a routed one. Used by the sniffer, embeds, YouTube thumbnails, link previews, embed images and avatars - so media still embeds on Tor-routed services, it just arrives over Tor. 4. **Test:** a check that while a routed account's messages are on screen, the default session makes no request to any outside host (`session.webRequest.onBeforeRequest` on the default session, in the layout-check harness against the fake IRC server with the account routed). Until this lands, the sniffer at least should be off for routed accounts.
Salastil added this to the v1.0 milestone 2026-10-02 19:41:03 -04:00
Salastil added the bugcriticalCVE labels 2026-10-02 19:41:03 -04:00
Author
Owner

Finished.

Each account now has a three-position route control instead of the Tor switch: Clearnet, Service tunnel, Strict.

On Strict, every remote URL the window loads for that account's buffers goes through the account's route (moho's Tor or the configured SOCKS5 proxy). That covers message media, embeds, avatars, emoji, reactions and the link check. Hostnames are resolved by the proxy. It fails closed: if there is no route yet, the picture doesn't load, and nothing is fetched directly.

Tested with a logging SOCKS proxy and web server. A link posted into a strict IRC channel reached the server only through the proxy, and the window made no direct requests.

Finished. Each account now has a three-position route control instead of the Tor switch: **Clearnet**, **Service tunnel**, **Strict**. On Strict, every remote URL the window loads for that account's buffers goes through the account's route (moho's Tor or the configured SOCKS5 proxy). That covers message media, embeds, avatars, emoji, reactions and the link check. Hostnames are resolved by the proxy. It fails closed: if there is no route yet, the picture doesn't load, and nothing is fetched directly. Tested with a logging SOCKS proxy and web server. A link posted into a strict IRC channel reached the server only through the proxy, and the window made no direct requests. - moho: https://git.salastil.com/Salastil/moho/commit/cee8604 - nobilis: https://git.salastil.com/Salastil/nobilis/commit/a3897ba
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Salastil/moho#257