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]>
46 lines
2.4 KiB
JavaScript
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";
|
|
}
|