Skip to content

Commit f710dee

Browse files
committed
fix(security): removes dead script, adds remaining test coverage
- deletes orphaned scripts/verify-csp-hash.mjs (replaced by inline CI shell) - updates prek.toml and deploy.yml to use inline CSP hash check - adds tests for returnTo preserved on validateToken failure and token exchange failure - adds test for SENTRY_DSN: undefined (optional field coverage)
1 parent 18f4c32 commit f710dee

File tree

5 files changed

+59
-56
lines changed

5 files changed

+59
-56
lines changed

.github/workflows/deploy.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,16 @@ jobs:
1818
- run: pnpm install --frozen-lockfile
1919
- run: pnpm run typecheck
2020
- run: pnpm test
21-
- name: Verify CSP hash
22-
run: node scripts/verify-csp-hash.mjs
21+
- name: Verify CSP inline script hash
22+
run: |
23+
SCRIPT=$(sed -n 's/.*<script>\(.*\)<\/script>.*/\1/p' index.html)
24+
HASH=$(printf '%s' "$SCRIPT" | openssl dgst -sha256 -binary | base64)
25+
if ! grep -q "sha256-$HASH" public/_headers; then
26+
echo "::error::CSP hash mismatch! Inline script hash 'sha256-$HASH' not found in public/_headers"
27+
echo "Update the sha256 hash in public/_headers to match the inline script in index.html"
28+
exit 1
29+
fi
30+
echo "CSP hash verified: sha256-$HASH"
2331
- name: WAF smoke tests
2432
run: pnpm test:waf
2533
- run: pnpm run build

prek.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ priority = 0
3535
id = "csp-hash"
3636
name = "CSP hash verification"
3737
language = "system"
38-
entry = "node scripts/verify-csp-hash.mjs"
38+
entry = "bash -c 'SCRIPT=$(sed -n \"s/.*<script>\\(.*\\)<\\/script>.*/\\1/p\" index.html) && HASH=$(printf \"%s\" \"$SCRIPT\" | openssl dgst -sha256 -binary | base64) && grep -q \"sha256-$HASH\" public/_headers && echo \"CSP hash OK: sha256-$HASH\" || { echo \"CSP hash mismatch! sha256-$HASH not in public/_headers\"; exit 1; }'"
3939
pass_filenames = false
4040
always_run = true
4141
priority = 0

scripts/verify-csp-hash.mjs

Lines changed: 0 additions & 52 deletions
This file was deleted.

tests/components/OAuthCallback.test.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,47 @@ describe("OAuthCallback", () => {
341341
expect(sessionStorage.getItem(OAUTH_RETURN_TO_KEY)).toBe("/settings");
342342
});
343343

344+
it("OAUTH_RETURN_TO_KEY is preserved when validateToken returns false", async () => {
345+
sessionStorage.setItem(OAUTH_RETURN_TO_KEY, "/settings");
346+
setupValidState();
347+
setWindowSearch({ code: "fakecode", state: "teststate" });
348+
vi.stubGlobal(
349+
"fetch",
350+
vi.fn().mockResolvedValue({
351+
ok: true,
352+
json: async () => ({ access_token: "tok123" }),
353+
})
354+
);
355+
vi.mocked(authStore.validateToken).mockResolvedValue(false);
356+
357+
renderCallback();
358+
359+
await waitFor(() => {
360+
screen.getByText(/Could not verify token/i);
361+
});
362+
expect(sessionStorage.getItem(OAUTH_RETURN_TO_KEY)).toBe("/settings");
363+
});
364+
365+
it("OAUTH_RETURN_TO_KEY is preserved when token exchange fails", async () => {
366+
sessionStorage.setItem(OAUTH_RETURN_TO_KEY, "/settings");
367+
setupValidState();
368+
setWindowSearch({ code: "fakecode", state: "teststate" });
369+
vi.stubGlobal(
370+
"fetch",
371+
vi.fn().mockResolvedValue({
372+
ok: false,
373+
json: async () => ({ error: "bad_verification_code" }),
374+
})
375+
);
376+
377+
renderCallback();
378+
379+
await waitFor(() => {
380+
screen.getByText(/Failed to complete sign in/i);
381+
});
382+
expect(sessionStorage.getItem(OAUTH_RETURN_TO_KEY)).toBe("/settings");
383+
});
384+
344385
it("navigates to / when OAUTH_RETURN_TO_KEY is not set", async () => {
345386
setupSuccessfulCallback();
346387
renderCallback();

tests/worker/oauth.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -955,12 +955,18 @@ describe("Worker OAuth endpoint", () => {
955955
expect(log!.level).toBe("warn");
956956
});
957957

958-
it("returns 404 when SENTRY_DSN is not configured", async () => {
958+
it("returns 404 when SENTRY_DSN is empty string", async () => {
959959
const req = makeTunnelRequest(makeEnvelope(VALID_DSN));
960960
const res = await worker.fetch(req, makeEnv({ SENTRY_DSN: "" }));
961961
expect(res.status).toBe(404);
962962
});
963963

964+
it("returns 404 when SENTRY_DSN is undefined", async () => {
965+
const req = makeTunnelRequest(makeEnvelope(VALID_DSN));
966+
const res = await worker.fetch(req, makeEnv({ SENTRY_DSN: undefined as unknown as string }));
967+
expect(res.status).toBe(404);
968+
});
969+
964970
it("returns 502 when Sentry is unreachable", async () => {
965971
globalThis.fetch = vi.fn().mockRejectedValue(new Error("connection refused"));
966972

0 commit comments

Comments
 (0)