From a97e39f445fa06b64407b11f30e567419ab2d45c Mon Sep 17 00:00:00 2001 From: Mihai Ionescu <129006005+andududu@users.noreply.github.com> Date: Mon, 21 Sep 2026 20:39:40 +0100 Subject: [PATCH] http://jeview.localhost:4777/ opens the viewer, with no hosts entry Jeview answers only to the names this machine goes by, so that a page on another site cannot reach it through a name of its own. A name ending in .localhost is one of those names: the ending is reserved for the machine itself, browsers and macOS resolve it to loopback without being told, and no other site can have it. Names that only look like one (localhost.x.com, x.localhost.y.com) are refused as before. Co-Authored-By: Claude Fable 5.1 --- README.md | 3 ++- src/jeview.ts | 4 +++- test/jeview.test.ts | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5e15103..b11e9ee 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ Needs Node 24 or later, and nothing else: Jeview has no dependencies to install. ``` That finds a suitable Node (on your PATH, or installed by nvm or Homebrew), starts Jeview and opens the viewer at -http://127.0.0.1:4777/. `npm start` does the same without opening a browser. Add your TypeSafe API key (the key icon, +http://127.0.0.1:4777/ (or http://jeview.localhost:4777/, a name your browser already knows). `npm start` does the same +without opening a browser. Add your TypeSafe API key (the key icon, top right). The first time you open it, the viewer explains itself. ## See it working diff --git a/src/jeview.ts b/src/jeview.ts index 6933e17..217813d 100644 --- a/src/jeview.ts +++ b/src/jeview.ts @@ -24,7 +24,9 @@ const PAGE = 5000, IDS_LISTED = 1000; // summaries in one answer: a page of the const REQUEST_DROP = new Set(["host", "connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade", "te", "trailer", "content-length", "accept-encoding", "authorization", "cookie", "origin", "referer"]); // fetch has already decoded the body, so its length and encoding no longer describe what is sent on const RESPONSE_DROP = new Set(["connection", "keep-alive", "transfer-encoding", "content-length", "content-encoding"]); -const LOOPBACK = /^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/; +// the names this machine goes by. Any name ending in .localhost is one of them: that ending is reserved for the machine +// itself, so no other site can have it, and http://jeview.localhost:4777/ opens the viewer with no hosts entry +const LOOPBACK = /^(127\.0\.0\.1|\[::1\]|([a-z0-9-]+\.)*localhost)(:\d+)?$/; /** A page on another site can POST here without asking first, and the call would go out with the user's key. A browser * names the page's site in Origin ("null" for a sandboxed one); a caller that is not a page sends none. */ const foreign = (origin: string | undefined) => origin !== undefined && !LOOPBACK.test(origin.replace(/^https?:\/\//, "")); diff --git a/test/jeview.test.ts b/test/jeview.test.ts index 8b97a91..ae8d170 100644 --- a/test/jeview.test.ts +++ b/test/jeview.test.ts @@ -175,6 +175,9 @@ test("the viewer answers loopback hosts only, serves its page, and a non-JSON bo const status = (host: string) => new Promise((accept, reject) => httpRequest({ host: "127.0.0.1", port, path: "/_/api/records", headers: { host } }, (res) => { res.resume(); accept(res.statusCode ?? 0); }).on("error", reject).end()); assert.equal(await status(`localhost:${port}`), 200); assert.equal(await status("attacker.example"), 403); + // a name ending in .localhost is this machine and nobody else's; a name that only looks like one is not + assert.deepEqual(await Promise.all([`jeview.localhost:${port}`, "my.jeview.localhost"].map(status)), [200, 200]); + assert.deepEqual(await Promise.all(["localhost.attacker.example", `jeview.localhost.attacker.example:${port}`, "attackerlocalhost", "jeview.localhost@attacker.example", ".localhost"].map(status)), [403, 403, 403, 403, 403]); const page = await fetch(`${base}/`); assert.equal(page.headers.get("content-type"), "text/html; charset=utf-8"); assert.match(page.headers.get("content-security-policy") ?? "", /script-src 'self'/);