Public: anyone can read this; the crew can change it.

A signed-out 404 offers sign-in that comes back to the same address #4115

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/4111-a-signed-out-404-offers-sign-in-that-com → main 2 commits fork 2 threads · 2 unresolved

  • Merged. scout/4111-a-signed-out-404-offers-sign-in-that-com was merged into main as 1ea2508.

Review

Sign in to review this change.
3 files changed +40 −4
src/web/chrome.ts +9 −2
@@ -66,8 +66,13 @@ export function crumbsFor(repo: Repo): { label: string; href: string }[] {
6666 }
6767
6868 /** The one 404 in the product: same words for "no such thing" and "not
69 * yours to see". */
69 * yours to see". A signed-out GET also gets the way back in, to this very
70 * address; it is offered whether or not the thing exists, so it tells nothing. */
7071 export function notFoundPage(ctx: Ctx): Response {
72 const back =
73 ctx.auth.user === undefined && ctx.request.method === "GET"
74 ? btn("Sign in", { href: signInHref(ctx) })
75 : undefined;
7176 return page(
7277 {
7378 ctx,
@@ -77,7 +82,9 @@ export function notFoundPage(ctx: Ctx): Response {
7782 icon: "search",
7883 title: "Nothing here",
7984 note: "The page you asked for does not exist, or is not visible to you.",
80 action: btn("Go home", { href: "/", variant: "primary" }),
85 action: back
86 ? html`${btn("Go home", { href: "/", variant: "primary" })} ${back}`
87 : btn("Go home", { href: "/", variant: "primary" }),
8188 })}`,
8289 },
8390 404,
test/web-origins.test.ts +2 −1
@@ -597,7 +597,8 @@ describe("origins on the web", () => {
597597 const sameBut = (text: string): string =>
598598 text
599599 .replaceAll(/nonce="[^"]*"/g, 'nonce="N"')
600 .replaceAll(/here\?path=[^"&\s]*/g, "here?path=");
600 .replaceAll(/here\?path=[^"&\s]*/g, "here?path=")
601 .replaceAll(/next=[^"]*/g, "next=");
601602 expect(sameBut(await stale.text())).toBe(sameBut(await unknown.text()));
602603
603604 // A finished flow's link dies with it, and the code is not reissued.
test/web-ui.test.ts +29 −1
@@ -17,6 +17,7 @@ import { createApp, type App } from "../src/app.ts";
1717 import { getRepo } from "../src/core/repos.ts";
1818 import { listLabels, labelsFor, getIssue } from "../src/core/issues.ts";
1919 import { serve, type Servers } from "../src/http/server.ts";
20 import { safeNext } from "../src/web/pages/site.ts";
2021 import { shellJs } from "../src/web/client.ts";
2122 import { gitClientEnv } from "./helpers/git-client.ts";
2223 import { proveAll } from "./helpers/verified.ts";
@@ -299,10 +300,37 @@ describe("web ui", () => {
299300 const scrub = (t: string) =>
300301 t
301302 .replaceAll(/nonce-?="?[\w-]+/g, "nonce")
302 .replaceAll(/here\?path=[^"&\s]*/g, "here?path=");
303 .replaceAll(/here\?path=[^"&\s]*/g, "here?path=")
304 .replaceAll(/next=[^"]*/g, "next=");
303305 expect(scrub(await hidden.text())).toBe(scrub(await absent.text()));
304306 });
305307
308 test("a signed-out 404 offers sign-in back to the same address", async () => {
309 const asked = "/kim/secret?tab=x&y=1";
310 const out = await (await fetch(`${base}${asked}`)).text();
311 expect(out).toContain(
312 `href="/signin?next=${encodeURIComponent(asked).replaceAll("&", "&")}"`,

encodeURIComponent already turns & into %26, so this replaceAll never matches; remove it.

313 );
314 const hidden = await (await fetch(`${base}/kim/secret`)).text();
315 const absent = await (await fetch(`${base}/kim/nothing`)).text();
316 const scrub = (t: string) =>

Wrap this chain across lines like the scrub above it.

317 t.replaceAll(/nonce-?="?[\w-]+/g, "nonce").replaceAll(/here\?path=[^"&\s]*/g, "here?path=").replaceAll(/next=[^"]*/g, "");
318 expect(scrub(hidden)).toBe(scrub(absent));
319
320 const signedIn = await (
321 await fetch(`${base}/kim/nothing`, { headers: { cookie: stranger } })
322 ).text();
323 expect(signedIn).toContain("Nothing here");
324 expect(signedIn).not.toContain("/signin?next=");
325 });
326
327 test("safeNext refuses another origin", () => {
328 expect(safeNext("//evil.example/x")).toBeUndefined();
329 expect(safeNext("https://evil.example")).toBeUndefined();
330 expect(safeNext("/\\evil.example")).toBeUndefined();
331 expect(safeNext("/kim/secret?a=1")).toBe("/kim/secret?a=1");
332 });
333
306334 // ---- ref safety ----
307335
308336 test("tree and commits refuse a ref that is not a ref", async () => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs