import { describe, it, expect } from "vitest"; import PocketBase from "pocketbase"; import { APP_URL, DEMO, requireServices, signIn } from "./helpers"; /** * Regression guard for a live full-owner-impersonation hole. * * `getCurrentUser()` used to trust `pb.authStore.loadFromCookie()`'s decoded * payload directly. That call only base64-decodes the cookie locally — the * SDK holds no secret to check a signature against — so a completely forged * cookie (garbage signature, `role: "owner"` claim, an email nobody owns) * was accepted as a real owner session by every action and page in the app. * Verified exploitable against this exact code path before the fix: a raw * `Cookie` header, no browser, no password, full owner access. * * The fix makes `getCurrentUser()` call `authRefresh()`, which asks * PocketBase itself to confirm the token. If this test ever passes again * without the app being signed in as owner, that verification has been * removed or bypassed. */ /** A cookie shaped exactly like a real one, with a token PocketBase never issued. */ function forgedOwnerCookie(): string { const pb = new PocketBase("http://forged.invalid"); function b64url(obj: unknown): string { return Buffer.from(JSON.stringify(obj)).toString("base64url"); } const header = b64url({ alg: "HS256", typ: "JWT" }); const payload = b64url({ id: "forged-owner-id", type: "auth", collectionId: "_pb_users_auth_", exp: Math.floor(Date.now() / 1000) + 3600, }); const forgedToken = `${header}.${payload}.not_a_real_signature`; // Load a fabricated record onto the auth store so exportToCookie() embeds // it — this is exactly what a legitimate login also does, only here the // token and record were never issued by PocketBase. pb.authStore.save(forgedToken, { id: "forged-owner-id", email: "attacker@example.com", role: "owner", client: "", active: true, } as never); return pb.authStore.exportToCookie({}, "pb_auth"); } describe("a forged session cookie cannot impersonate the owner", () => { it("refuses a request bearing a self-signed, PocketBase-never-issued cookie", async () => { await requireServices(); const response = await fetch(`${APP_URL}/`, { headers: { cookie: forgedOwnerCookie() }, redirect: "manual", }); // A verified session check treats this as signed-out: redirected to // /login, never a 200 rendering the owner's home page. if (response.status >= 300 && response.status < 400) { expect(response.headers.get("location")).toContain("/login"); } else { // Some deployments render the redirect target directly rather than // a 3xx — either is acceptable, but the OWNER'S home page must not // have rendered. const html = await response.text(); expect(html).not.toContain("Everything you are building"); } }); it("still accepts a real, freshly signed-in session", async () => { await requireServices(); const cookie = await signIn(DEMO.ownerEmail, DEMO.ownerPassword); const response = await fetch(`${APP_URL}/`, { headers: { cookie }, }); expect(response.status).toBe(200); }); });