2cea84c3cba0
work out a sign-in's next path once, and restore the note on why login registers first (v3.0.72)
- time
- 2026-09-28 05:28 UTC · 2 weeks ago
- machine
- Junes-MacBook-Pro.local
- agent
- claude-opus-5-5
- session
- fix a2 review findings
- parent
- 4bc2782ec862
↓ kepr-2cea84c3.tar.gzatlas at this save
files changed3 files
package.json+1 −1src/server/app.ts+1 −0src/server/login.ts+15 −25package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "kepr",3- "version": "3.0.71",3+ "version": "3.0.72",44 "private": true,55 "type": "module",66 "scripts": {src/server/app.ts+1 −0
@@ -88,6 +88,7 @@8888 });8989 const ctx = { domain: config.domain, kepr, mirrorDir: join(config.dataDir, "mirror") };9090 const bucket = openBucket(config.bucket);91+ // Before every other route, so the admin guard runs ahead of each `/admin` handler.9192 registerLogin(app, ctx, config.adminPasswordHash);9293 registerConnect(app, config, ctx);9394 const admin = {src/server/login.ts+15 −25
@@ -93,12 +93,11 @@9393 c.header("Cache-Control", "no-store");9494 });9595 96- app.get("/sign-in", (c) =>97- signInPage(c, ctx, "", 200, validNext(c.req.query("next") ?? "") ?? "/admin"),98- );96+ app.get("/sign-in", (c) => signInPage(c, ctx, "", 200, nextPath(c.req.query("next"))));9997 10098 app.post("/sign-in", async (c) => {10199 const fields = await c.req.parseBody();100+ const next = nextPath(fields.next);102101 const cookie = getCookie(c, SIGN_IN_COOKIE) ?? "";103102 const form = typeof fields.form === "string" ? fields.form : "";104103 if (c.req.header("origin") !== origin || !HEX64.test(cookie) || !sameText(form, cookie)) {@@ -110,24 +109,12 @@110109 const mine = failures.filter((failure) => failure.address === address).length;111110 if (mine >= FAILURES_PER_ADDRESS) {112111 c.header("Retry-After", String(FAILURE_WINDOW_MS / 1000));113- return signInPage(114- c,115- ctx,116- "Too many attempts. Try again later.",117- 429,118- validNext(typeof fields.next === "string" ? fields.next : "") ?? "/admin",119- );112+ return signInPage(c, ctx, "Too many attempts. Try again later.", 429, next);120113 }121114 const password = typeof fields.password === "string" ? fields.password : "";122115 if (!(await Bun.password.verify(password, passwordHash))) {123116 failures.push({ at: now, address });124- return signInPage(125- c,126- ctx,127- "That password is not right.",128- 401,129- validNext(typeof fields.next === "string" ? fields.next : "") ?? "/admin",130- );117+ return signInPage(c, ctx, "That password is not right.", 401, next);131118 }132119 const token = randomBytes(32).toString("hex");133120 const at = nowSeconds();@@ -137,10 +124,7 @@137124 .run(sha256(token), at + SESSION_SECONDS);138125 setCookie(c, SESSION_COOKIE, token, { ...COOKIE, maxAge: SESSION_SECONDS });139126 deleteCookie(c, SIGN_IN_COOKIE, COOKIE);140- return c.redirect(141- validNext(typeof fields.next === "string" ? fields.next : "") ?? "/admin",142- 303,143- );127+ return c.redirect(next, 303);144128 });145129 146130 app.post("/admin/sign-out", (c) => {@@ -152,19 +136,25 @@152136 });153137 }154138 155-function validNext(value: string): string | null {156- if (!value.startsWith("/") || value.startsWith("//") || value.startsWith("/\\")) return null;139+/**140+ * Where a sign-in goes on to: a path on this site, or `/admin`. `//host` and `/\host` are other141+ * sites to a browser, and a control character could split the Location header.142+ */143+function nextPath(value: unknown): string {144+ if (typeof value !== "string" || !value.startsWith("/")) return "/admin";145+ if (value.startsWith("//") || value.startsWith("/\\")) return "/admin";157146 if ([...value].some((char) => char.charCodeAt(0) <= 31 || char.charCodeAt(0) === 127))158- return null;147+ return "/admin";159148 return value;160149 }150+161151 // A double-submit token repeats a SameSite=Strict cookie that a cross-site post never has.162152 function signInPage(163153 c: Context,164154 ctx: PageContext,165155 error: string,166156 status: 200 | 401 | 429,167- next = "/admin",157+ next: string,168158 ) {169159 let cookie = getCookie(c, SIGN_IN_COOKIE) ?? "";170160 if (!HEX64.test(cookie)) {