Skip to content

Commit a28a80d

Browse files
committed
fix(security): adds Turnstile domains to CSP, removes dead body.code path, adds rate limiter error test
1 parent 39215e4 commit a28a80d

File tree

4 files changed

+19
-18
lines changed

4 files changed

+19
-18
lines changed

public/_headers

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Content-Security-Policy: default-src 'none'; script-src 'self' 'sha256-uEFqyYCMaNy1Su5VmWLZ1hOCRBjkhm4+ieHHxQW6d3Y='; style-src-elem 'self'; style-src-attr 'unsafe-inline'; img-src 'self' data: https://avatars.githubusercontent.com; connect-src 'self' https://api.github.com ws://127.0.0.1:*; font-src 'self'; worker-src 'self'; manifest-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'none'; upgrade-insecure-requests; report-uri /api/csp-report; report-to csp-endpoint
2+
Content-Security-Policy: default-src 'none'; script-src 'self' 'sha256-uEFqyYCMaNy1Su5VmWLZ1hOCRBjkhm4+ieHHxQW6d3Y=' https://challenges.cloudflare.com; style-src-elem 'self'; style-src-attr 'unsafe-inline'; img-src 'self' data: https://avatars.githubusercontent.com; connect-src 'self' https://api.github.com ws://127.0.0.1:*; font-src 'self'; worker-src 'self'; manifest-src 'self'; frame-src https://challenges.cloudflare.com; frame-ancestors 'none'; base-uri 'self'; form-action 'none'; upgrade-insecure-requests; report-uri /api/csp-report; report-to csp-endpoint
33
Reporting-Endpoints: csp-endpoint="/api/csp-report"
44
X-Content-Type-Options: nosniff
55
Referrer-Policy: strict-origin-when-cross-origin

src/app/lib/proxy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export async function sealApiToken(token: string, purpose: string): Promise<stri
118118
let message = "unknown_error";
119119
try {
120120
const body = (await res.json()) as { code?: string; error?: string };
121-
message = body.code ?? body.error ?? message;
121+
message = body.error ?? message;
122122
} catch {
123123
// ignore parse errors — keep default message
124124
}

tests/app/lib/proxy.test.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -288,20 +288,6 @@ describe("sealApiToken", () => {
288288
it("throws { status, message } on 403 response", async () => {
289289
setupMockedTurnstile("turnstile-tok");
290290

291-
const mockFetch = vi.fn().mockResolvedValue(
292-
new Response(JSON.stringify({ code: "turnstile_failed" }), { status: 403 }),
293-
);
294-
vi.stubGlobal("fetch", mockFetch);
295-
296-
await expect(mod.sealApiToken("my-token", "jira-api-token")).rejects.toMatchObject({
297-
status: 403,
298-
message: "turnstile_failed",
299-
});
300-
});
301-
302-
it("throws { status, message } on 403 response using error field", async () => {
303-
setupMockedTurnstile("turnstile-tok");
304-
305291
const mockFetch = vi.fn().mockResolvedValue(
306292
new Response(JSON.stringify({ error: "turnstile_failed" }), { status: 403 }),
307293
);
@@ -317,7 +303,7 @@ describe("sealApiToken", () => {
317303
setupMockedTurnstile("turnstile-tok");
318304

319305
const mockFetch = vi.fn().mockResolvedValue(
320-
new Response(JSON.stringify({ code: "rate_limited" }), { status: 429 }),
306+
new Response(JSON.stringify({ error: "rate_limited" }), { status: 429 }),
321307
);
322308
vi.stubGlobal("fetch", mockFetch);
323309

@@ -331,7 +317,7 @@ describe("sealApiToken", () => {
331317
setupMockedTurnstile("turnstile-tok");
332318

333319
const mockFetch = vi.fn().mockResolvedValue(
334-
new Response(JSON.stringify({ code: "seal_failed" }), { status: 500 }),
320+
new Response(JSON.stringify({ error: "seal_failed" }), { status: 500 }),
335321
);
336322
vi.stubGlobal("fetch", mockFetch);
337323

tests/worker/seal.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,21 @@ describe("Worker /api/proxy/seal endpoint", () => {
171171
expect(res.headers.get("Retry-After")).toBe("60");
172172
});
173173

174+
it("request proceeds when rate limiter throws (fail-open)", async () => {
175+
globalThis.fetch = vi.fn().mockResolvedValue(
176+
new Response(JSON.stringify({ success: true }), { status: 200 })
177+
);
178+
179+
const rateLimiter = { limit: vi.fn().mockRejectedValue(new Error("binding unavailable")) };
180+
const req = makeSealRequest();
181+
const res = await worker.fetch(req, makeEnv({ PROXY_RATE_LIMITER: rateLimiter }));
182+
183+
// Should NOT be 429 or 500 — rate limiter failure is fail-open
184+
expect(res.status).toBe(200);
185+
const json = await res.json() as Record<string, unknown>;
186+
expect(json["sealed"]).toBeDefined();
187+
});
188+
174189
// ── Input validation ──────────────────────────────────────────────────────
175190

176191
it("request with token exceeding 2048 chars returns 400 with invalid_request", async () => {

0 commit comments

Comments
 (0)