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
3 files package.json+1 −1src/server/app.ts+1 −0src/server/login.ts+15 −25
package.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": {

view file

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 = {

view file

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)) {

view file