Skip to content
File

Blob: test/admin-csrf.worker.test.ts

typescript108 lines
1import { beforeAll, describe, expect, it } from "vitest";
2import { env, exports as workerExports } from "cloudflare:workers";
3 
4import { ensureD1Migrations } from "./util/d1Setup";
5import { setupRepoForTests } from "./util/repoSeed";
6 
7beforeAll(async () => {
8 await ensureD1Migrations(env);
9});
10 
11type Mutating = {
12 label: string;
13 method: "POST" | "PUT" | "DELETE";
14 path: (owner: string, repo: string) => string;
15 body?: unknown;
16};
17 
18const MUTATING_ROUTES: Mutating[] = [
19 {
20 label: "compaction POST",
21 method: "POST",
22 path: (o, r) => `/${o}/${r}/admin/compact`,
23 body: {},
24 },
25 {
26 label: "compaction DELETE",
27 method: "DELETE",
28 path: (o, r) => `/${o}/${r}/admin/compact`,
29 },
30 {
31 label: "refs PUT",
32 method: "PUT",
33 path: (o, r) => `/${o}/${r}/admin/refs`,
34 body: [],
35 },
36 {
37 label: "head PUT",
38 method: "PUT",
39 path: (o, r) => `/${o}/${r}/admin/head`,
40 body: { target: "refs/heads/main" },
41 },
42 {
43 label: "pack DELETE",
44 method: "DELETE",
45 path: (o, r) => `/${o}/${r}/admin/pack/pack-deadbeef.pack`,
46 },
47 {
48 label: "purge DELETE",
49 method: "DELETE",
50 path: (o, r) => `/${o}/${r}/admin/purge`,
51 body: { confirm: "purge-PLACEHOLDER" },
52 },
53];
54 
55async function call(opts: {
56 url: string;
57 method: "POST" | "PUT" | "DELETE";
58 cookieHeader: string;
59 origin?: string;
60 body?: unknown;
61}): Promise<Response> {
62 const headers: Record<string, string> = { Cookie: opts.cookieHeader };
63 if (opts.body !== undefined) headers["Content-Type"] = "application/json";
64 if (opts.origin) headers.Origin = opts.origin;
65 return await workerExports.default.fetch(opts.url, {
66 method: opts.method,
67 headers,
68 body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
69 });
70}
71 
72describe("admin mutating routes: CSRF (sameOriginViolation)", () => {
73 for (const route of MUTATING_ROUTES) {
74 it(`${route.label}: missing Origin -> 403`, async () => {
75 const owner = `csrf-${Math.random().toString(36).slice(2, 8)}`;
76 const repo = "site";
77 const seeded = await setupRepoForTests(env, owner, repo);
78 const url = `https://example.com${route.path(owner, repo)}`;
79 const body =
80 route.label === "purge DELETE" ? { confirm: `purge-${owner}/${repo}` } : route.body;
81 const res = await call({
82 url,
83 method: route.method,
84 cookieHeader: seeded.cookieHeader,
85 body,
86 });
87 expect(res.status).toBe(403);
88 });
89 
90 it(`${route.label}: cross-origin Origin -> 403`, async () => {
91 const owner = `csrf-${Math.random().toString(36).slice(2, 8)}`;
92 const repo = "site";
93 const seeded = await setupRepoForTests(env, owner, repo);
94 const url = `https://example.com${route.path(owner, repo)}`;
95 const body =
96 route.label === "purge DELETE" ? { confirm: `purge-${owner}/${repo}` } : route.body;
97 const res = await call({
98 url,
99 method: route.method,
100 cookieHeader: seeded.cookieHeader,
101 origin: "https://attacker.example",
102 body,
103 });
104 expect(res.status).toBe(403);
105 });
106 }
107});