Skip to content

Commit 569dcff

Browse files
Merge branch 'main' into fix/stale-retention-salvageability-tests
2 parents aa015fd + 995cd78 commit 569dcff

27 files changed

Lines changed: 1068 additions & 83 deletions

apps/loopover-miner-ui/src/auth.test.ts

Lines changed: 117 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { describe, expect, it } from "vitest";
22

3-
import { authPlugin, handleAuthRequest, isAuthenticatedRequest } from "../vite-auth";
3+
import {
4+
AUTH_BOOTSTRAP_PATH,
5+
authPlugin,
6+
handleAuthRequest,
7+
handleBootstrapRequest,
8+
isAuthenticatedRequest,
9+
} from "../vite-auth";
410

511
const TOKEN = "deterministic-test-token";
612

@@ -25,6 +31,13 @@ describe("isAuthenticatedRequest (#4858)", () => {
2531
expect(isAuthenticatedRequest("loopover_miner_ui_token=wrong-value", TOKEN)).toBe(false);
2632
});
2733

34+
it("rejects the auth cookie when its value is a different length than the server's token", () => {
35+
// Exercises the length-mismatch short-circuit ahead of timingSafeEqual, which throws on unequal-length
36+
// buffers -- a shorter or longer guess must be rejected, not crash the request handler.
37+
expect(isAuthenticatedRequest("loopover_miner_ui_token=short", TOKEN)).toBe(false);
38+
expect(isAuthenticatedRequest(`loopover_miner_ui_token=${TOKEN}-and-then-some`, TOKEN)).toBe(false);
39+
});
40+
2841
it("accepts the auth cookie when its value matches the server's token exactly", () => {
2942
expect(isAuthenticatedRequest(`loopover_miner_ui_token=${TOKEN}`, TOKEN)).toBe(true);
3043
});
@@ -52,15 +65,46 @@ describe("handleAuthRequest (#4858)", () => {
5265
});
5366
});
5467

68+
describe("handleBootstrapRequest (GHSA-v6v4-mh5m-5mqq)", () => {
69+
it("falls through (null) for a request with no url", () => {
70+
expect(handleBootstrapRequest(undefined, TOKEN)).toBeNull();
71+
});
72+
73+
it("falls through (null) for any path other than the bootstrap path", () => {
74+
expect(handleBootstrapRequest("/", TOKEN)).toBeNull();
75+
expect(handleBootstrapRequest("/api/portfolio-queue", TOKEN)).toBeNull();
76+
});
77+
78+
it("redirects to / when the bootstrap path is visited with the correct token", () => {
79+
expect(handleBootstrapRequest(`${AUTH_BOOTSTRAP_PATH}?token=${TOKEN}`, TOKEN)).toEqual({ redirectTo: "/" });
80+
});
81+
82+
it("returns a 404 (not a 401) for the bootstrap path with a missing token", () => {
83+
expect(handleBootstrapRequest(AUTH_BOOTSTRAP_PATH, TOKEN)).toEqual({ status: 404, body: "Not Found" });
84+
});
85+
86+
it("returns a 404 for the bootstrap path with the wrong token", () => {
87+
expect(handleBootstrapRequest(`${AUTH_BOOTSTRAP_PATH}?token=wrong`, TOKEN)).toEqual({
88+
status: 404,
89+
body: "Not Found",
90+
});
91+
});
92+
});
93+
5594
type CapturedRequestHandler = (
5695
req: { url?: string; headers: { cookie?: string } },
5796
res: { statusCode: number; setHeader: (k: string, v: string) => void; end: (body: string) => void },
5897
next: () => void,
5998
) => void;
6099

61-
function captureMiddleware(deps = { generateToken: () => TOKEN }): CapturedRequestHandler {
100+
function captureMiddleware(
101+
deps: { generateToken?: () => string; logToken?: (token: string) => void } = {
102+
generateToken: () => TOKEN,
103+
logToken: () => {},
104+
},
105+
): CapturedRequestHandler {
62106
let captured: CapturedRequestHandler | undefined;
63-
const plugin = authPlugin(deps);
107+
const plugin = authPlugin({ logToken: () => {}, ...deps });
64108
const server = { middlewares: { use: (fn: CapturedRequestHandler) => (captured = fn) } };
65109
// @ts-expect-error -- the test double only implements the subset of Vite's ViteDevServer this plugin reads.
66110
plugin.configureServer(server);
@@ -93,23 +137,21 @@ function fakeResponse() {
93137
};
94138
}
95139

96-
describe("authPlugin (#4858)", () => {
97-
it("stamps the Set-Cookie header on a non-/api/ request that falls through (the initial page load)", () => {
140+
describe("authPlugin (#4858, hardened for GHSA-v6v4-mh5m-5mqq)", () => {
141+
it("regression: NEVER stamps Set-Cookie on an unauthenticated, non-bootstrap request -- not even the initial page load", () => {
142+
// This is the exact vulnerability: an earlier version stamped Set-Cookie on every response that
143+
// wasn't itself rejected, including a plain unauthenticated `GET /`. That let ANY local process --
144+
// not just the intended browser -- read a valid, replayable session token off one unauthenticated
145+
// request (`curl http://127.0.0.1:4174/`), then use it against the mutating /api/* routes.
98146
const middleware = captureMiddleware();
99-
const { res, headers } = fakeResponse();
100-
let calledNext = false;
101-
middleware({ url: "/", headers: {} }, res, () => {
102-
calledNext = true;
103-
});
104-
expect(headers["Set-Cookie"]).toBe(`loopover_miner_ui_token=${TOKEN}; HttpOnly; SameSite=Strict; Path=/`);
105-
expect(calledNext).toBe(true);
147+
for (const url of ["/", "/index.html", "/assets/index.js", "/api/portfolio-queue"]) {
148+
const { res, headers } = fakeResponse();
149+
middleware({ url, headers: {} }, res, () => {});
150+
expect(headers["Set-Cookie"], `Set-Cookie must be absent for unauthenticated ${url}`).toBeUndefined();
151+
}
106152
});
107153

108-
it("rejects an unauthenticated /api/* request with 401, never calls next(), and NEVER leaks the token via Set-Cookie", () => {
109-
// Regression test: an earlier version set Set-Cookie on every response BEFORE checking auth, so an
110-
// unauthenticated caller could read the valid token straight off this 401's own headers and replay it --
111-
// completely defeating the mechanism. The token must only ever reach a caller that is already
112-
// authenticated, or a non-/api/* (page/asset) request.
154+
it("rejects an unauthenticated /api/* request with 401, never calls next(), and never leaks the token via Set-Cookie", () => {
113155
const middleware = captureMiddleware();
114156
const { res, headers, getEnded, getStatus } = fakeResponse();
115157
let calledNext = false;
@@ -135,6 +177,44 @@ describe("authPlugin (#4858)", () => {
135177
expect(headers["Set-Cookie"]).toBe(`loopover_miner_ui_token=${TOKEN}; HttpOnly; SameSite=Strict; Path=/`);
136178
});
137179

180+
it("re-stamps the cookie on an already-authenticated non-/api/ request too (keeps the session rolling)", () => {
181+
const middleware = captureMiddleware();
182+
const { res, headers } = fakeResponse();
183+
let calledNext = false;
184+
middleware({ url: "/", headers: { cookie: `loopover_miner_ui_token=${TOKEN}` } }, res, () => {
185+
calledNext = true;
186+
});
187+
expect(calledNext).toBe(true);
188+
expect(headers["Set-Cookie"]).toBe(`loopover_miner_ui_token=${TOKEN}; HttpOnly; SameSite=Strict; Path=/`);
189+
});
190+
191+
it("mints the cookie and redirects to / when the bootstrap path is visited with the correct token", () => {
192+
const middleware = captureMiddleware();
193+
const { res, headers, getStatus } = fakeResponse();
194+
let calledNext = false;
195+
middleware({ url: `/__auth/bootstrap?token=${TOKEN}`, headers: {} }, res, () => {
196+
calledNext = true;
197+
});
198+
expect(getStatus()).toBe(302);
199+
expect(headers.Location).toBe("/");
200+
expect(headers["Set-Cookie"]).toBe(`loopover_miner_ui_token=${TOKEN}; HttpOnly; SameSite=Strict; Path=/`);
201+
expect(calledNext).toBe(false);
202+
});
203+
204+
it("404s the bootstrap path (never sets a cookie) when the token is missing or wrong", () => {
205+
const middleware = captureMiddleware();
206+
207+
const missing = fakeResponse();
208+
middleware({ url: "/__auth/bootstrap", headers: {} }, missing.res, () => {});
209+
expect(missing.getStatus()).toBe(404);
210+
expect(missing.headers["Set-Cookie"]).toBeUndefined();
211+
212+
const wrong = fakeResponse();
213+
middleware({ url: "/__auth/bootstrap?token=wrong", headers: {} }, wrong.res, () => {});
214+
expect(wrong.getStatus()).toBe(404);
215+
expect(wrong.headers["Set-Cookie"]).toBeUndefined();
216+
});
217+
138218
it("uses deps.generateToken so a fixed test token is deterministic across requests", () => {
139219
const middleware = captureMiddleware({ generateToken: () => "fixed-token-123" });
140220
const { res } = fakeResponse();
@@ -145,9 +225,28 @@ describe("authPlugin (#4858)", () => {
145225
expect(calledNext).toBe(true);
146226
});
147227

228+
it("calls deps.logToken once with the generated token so an operator can complete the bootstrap flow", () => {
229+
const logged: string[] = [];
230+
authPlugin({ generateToken: () => "logged-token", logToken: (token) => logged.push(token) });
231+
expect(logged).toEqual(["logged-token"]);
232+
});
233+
234+
it("uses console.log as the default logToken when no override is given", () => {
235+
const originalLog = console.log;
236+
const calls: unknown[][] = [];
237+
console.log = (...args: unknown[]) => calls.push(args);
238+
try {
239+
authPlugin({ generateToken: () => "console-logged-token" });
240+
} finally {
241+
console.log = originalLog;
242+
}
243+
expect(calls).toHaveLength(1);
244+
expect(String(calls[0]?.[0])).toContain("console-logged-token");
245+
});
246+
148247
it("also attaches via configurePreviewServer for `vite preview`", () => {
149248
let captured: CapturedRequestHandler | undefined;
150-
const plugin = authPlugin();
249+
const plugin = authPlugin({ logToken: () => {} });
151250
const server = { middlewares: { use: (fn: CapturedRequestHandler) => (captured = fn) } };
152251
// @ts-expect-error -- same partial test double as configureServer above.
153252
plugin.configurePreviewServer(server);

0 commit comments

Comments
 (0)