signup steps: journey() conflates page identity with query status #123

Stängd
öppnade 2026-07-31 18:46:26 +00:00 av supernaut · 1 kommentar
Ägare

journey() takes a single string that means two different things, and the two can contradict each other.

src/lib/signup-steps.ts derives step state from the sign-up page's ?status= query parameter. But /welcome is not a status — it is a page — and it was threaded through the same argument:

if (status === "welcome") {
  return STEP_IDS.map((id) => ({ id, state: id === "start" ? "current" : "done" }));
}

So the parameter is now "status, or sometimes a page name". Visiting /signup?status=welcome by hand renders "steps 1-3 done, step 4 current" above a complete, empty sign-up form — a state that cannot be reached legitimately and makes no sense.

Why it is minor

Only reachable by editing the URL. Nothing is disclosed, nothing breaks, and journey() is total over any string so it cannot throw. Cosmetic incoherence.

Why it is worth fixing anyway

The conflation will spread. The next surface that needs a step state (a payment step, an admin view) faces the same choice, and the honest answer keeps being "invent another pseudo-status". A clearer seam:

journey({ page: "signup" | "welcome", status: null | string })

The page says where the user is; the status says what happened there. /signup?status=welcome then renders step 1 current, as it should, because the page is signup regardless of the query string.

That is a small refactor with a mechanical test update — the existing tests already cover every status, so they would gain a page argument and one new case asserting that an unexpected status on the sign-up page cannot fake progress.

Done when

A query parameter cannot make the sign-up page claim the journey is nearly complete.

`journey()` takes a single string that means two different things, and the two can contradict each other. `src/lib/signup-steps.ts` derives step state from the sign-up page's `?status=` query parameter. But `/welcome` is not a status — it is a page — and it was threaded through the same argument: ```ts if (status === "welcome") { return STEP_IDS.map((id) => ({ id, state: id === "start" ? "current" : "done" })); } ``` So the parameter is now "status, or sometimes a page name". Visiting `/signup?status=welcome` by hand renders "steps 1-3 done, step 4 current" above a complete, empty sign-up form — a state that cannot be reached legitimately and makes no sense. ## Why it is minor Only reachable by editing the URL. Nothing is disclosed, nothing breaks, and `journey()` is total over any string so it cannot throw. Cosmetic incoherence. ## Why it is worth fixing anyway The conflation will spread. The next surface that needs a step state (a payment step, an admin view) faces the same choice, and the honest answer keeps being "invent another pseudo-status". A clearer seam: ```ts journey({ page: "signup" | "welcome", status: null | string }) ``` The page says where the user is; the status says what happened there. `/signup?status=welcome` then renders step 1 current, as it should, because the page is `signup` regardless of the query string. That is a small refactor with a mechanical test update — the existing tests already cover every status, so they would gain a page argument and one new case asserting that an unexpected status on the sign-up page cannot fake progress. ## Done when A query parameter cannot make the sign-up page claim the journey is nearly complete.
supernaut lade till detta till projektet Bitborg Web 2026-07-31 18:46:48 +00:00
Upphovsperson
Ägare

Already done — shipped by 8439fa4 (PR #125); its close keywords never fired.

Verified at `src/lib/signup-steps.ts:52`: the signature is now `journey({ page, status })`, with
page identity separated from query status and the attacker-controlled `status` documented as such.
A regression test covers it at `src/lib/signup-steps.test.ts:18-37`.

Closing as already implemented.

Already done — shipped by 8439fa4 (PR #125); its close keywords never fired. Verified at \`src/lib/signup-steps.ts:52\`: the signature is now \`journey({ page, status })\`, with page identity separated from query status and the attacker-controlled \`status\` documented as such. A regression test covers it at \`src/lib/signup-steps.test.ts:18-37\`. Closing as already implemented.
Logga in för att delta i denna konversation.
Ingen milstolpe
Inget projekt
Inga tilldelade
1 deltagare
Notiser
Förfallodatum
Förfallodatumet är ogiltigt eller utanför gränserna. Använd formatet "åååå-mm-dd".

Inget förfallodatum satt.

Beroenden

Inga beroenden satta

Referens
bitborg/bitborg-web#123
Ingen beskrivning angiven.