open-standards/joule-code/joule-code-rs/crates/joule-moq-web/web/room/endpoint.js
Transaction Science cb766e7166 room page: take the endpoints a link names only on the page's own origin
The room client took its relay, room service, fixture, WebTransport URL,
certificate hashes and issuer directory from its query string as given, and
followed a relay's GOAWAY to any URI. A crafted link and a press of Join would
send a joiner's camera and microphone to another origin.

- web/room/endpoint.js: an endpoint is taken only on the page's own origin
  (`https://<host>`, or `wss://<host>` for a relay), never with user
  information; a page served from the loopback interface (a fixture page, the
  e2e harness) takes loopback addresses alone.
- app.js: `service`, `ws`, `wt`, `fixture` and `issuer_directory` are held to
  it; `hashes` only on a loopback page. A refused endpoint is recorded
  (`snapshot().refused`) and shown, Join stays off, and the page connects to
  nothing: `enter()` checks the relay before it asks for the camera, a grant or
  a session. A GOAWAY naming another origin is refused: the page closes its
  session and says so.
- web/room/endpoint.test.mjs (run by e2e/run.sh): own origin, another origin,
  scheme-relative, the page's host as user information, javascript:, a plain
  scheme on a secure page, an HTTP page, and loopback for the harness.
- e2e scenario `origin`: a foreign `?ws=` is refused at Join and nothing
  connects (no WebSocket, request, grant or camera); a foreign `?service=`,
  scheme-relative, behind user information or as javascript:, is refused with
  Join off; a GOAWAY to another origin is refused and the session ends. The
  `service` scenario checks that every request and WebSocket of every page
  stays on the page's origin, as the production `connect-src` requires.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 12:38:31 -04:00

46 lines
2.4 KiB
JavaScript

// Which endpoints the room page takes from outside its own code: a relay, a room service, a
// fixture or an issuer directory its query string names, and the URI a relay's GOAWAY moves
// the session to. A page that took any of them as given could be pointed, by a link, at
// another origin, and a visitor who joined from it would send their camera and microphone
// there. So an endpoint is taken only when it is the page's own origin: `https://<the page's
// host>`, or `wss://<the page's host>` for a relay. A page served from the loopback interface
// (a fixture page, or the end-to-end harness, whose relays and services listen on other
// loopback ports) takes a loopback endpoint, and nothing else.
/** The host names of the loopback interface, as `URL.hostname` writes them. */
export const LOOPBACK_HOSTS = ["localhost", "127.0.0.1", "[::1]"];
export function isLoopback(hostname) {
return LOOPBACK_HOSTS.includes(hostname);
}
/**
* Whether the page at `page` (a URL, or `location`) takes `value` as an endpoint of `kind`:
* "http" for a fetch or a WebTransport session, "ws" for a WebSocket relay. A relative value
* is resolved against the page. Returns `{ ok: true, url }` with the endpoint in full, or
* `{ ok: false, reason }`.
*/
export function checkEndpoint(value, kind, page) {
const p = new URL(String(page));
let u;
try {
u = new URL(String(value), p.href);
} catch {
return { ok: false, reason: "not a URL" };
}
const schemes = kind === "ws" ? ["wss:", "ws:"] : ["https:", "http:"];
if (!schemes.includes(u.protocol)) return { ok: false, reason: `the scheme ${u.protocol} is not a ${kind === "ws" ? "WebSocket" : "web"} endpoint's` };
// User information before a host reads as a host to a person and names none: never taken.
if (u.username || u.password) return { ok: false, reason: "it carries user information" };
if (isLoopback(p.hostname)) {
return isLoopback(u.hostname) ? { ok: true, url: u.href } : { ok: false, reason: `a loopback page takes only loopback endpoints, not ${u.host}` };
}
const secure = kind === "ws" ? "wss:" : "https:";
if (p.protocol !== "https:" || u.protocol !== secure || u.host !== p.host) return { ok: false, reason: `${u.protocol}//${u.host} is not this page's origin` };
return { ok: true, url: u.href };
}
/** The kind of endpoint a URI is by its scheme: "ws" for ws: and wss:, else "http". */
export function kindOf(value) {
return /^wss?:/i.test(String(value)) ? "ws" : "http";
}