SPB Git
5.5 KB · 36 lines typescript
1/** Black-box security checks against a running instance: auth boundaries, IDOR, CSRF, SSRF guard, injection payloads, header policy, private-data leakage. Exits 1 on any failure. */2const base = (process.env.E2E_BASE_URL ?? "http://127.0.0.1:8540").replace(/\/$/, "");3type Check = { name: string; ok: boolean; detail?: string };4const checks: Check[] = [];5const get = (p: string, init: RequestInit = {}) => fetch(base + p, { redirect: "manual", ...init });6async function run() {7  // 1. unauthenticated admin access8  for (const p of ["/admin", "/admin/tools", "/admin/owner", "/admin/settings"]) { const r = await get(p); checks.push({ name: `unauth ${p} redirects to login`, ok: r.status === 307 && (r.headers.get("location") ?? "").includes("/login"), detail: `${r.status} ${r.headers.get("location")}` }); }9  for (const p of ["/api/admin/stats", "/api/admin/tools", "/api/admin/export?scope=all", "/api/admin/owner/00000000-0000-0000-0000-000000000000", "/api/admin/audit", "/api/auth/sessions"]) { const r = await get(p); checks.push({ name: `unauth ${p} → 401`, ok: r.status === 401, detail: String(r.status) }); }10  for (const [p, m] of [["/api/admin/tools", "POST"], ["/api/admin/jobs", "POST"], ["/api/admin/import", "POST"], ["/api/admin/collections", "DELETE"], ["/api/auth/password", "POST"]] as const) { const r = await get(p, { method: m, headers: { "content-type": "application/json" }, body: "{}" }); checks.push({ name: `unauth ${m} ${p} → 401/403`, ok: r.status === 401 || r.status === 403, detail: String(r.status) }); }11  // 2. cross-site login attempt (Origin mismatch / sec-fetch-site cross-site) → 40312  { const r = await get("/api/auth/login", { method: "POST", headers: { "content-type": "application/json", origin: "https://evil.example", "sec-fetch-site": "cross-site" }, body: JSON.stringify({ email: "a@b.co", password: "x" }) }); checks.push({ name: "cross-site login blocked", ok: r.status === 403, detail: String(r.status) }); }13  // 3. SQL injection / hostile input in public search & suggest14  for (const q of ["' OR 1=1 --", "\" ; DROP TABLE tools; --", "<script>alert(1)</script>", "%00", "🙂".repeat(50), "a".repeat(500)]) { const r = await get(`/tools?q=${encodeURIComponent(q)}`); const html = await r.text(); checks.push({ name: `search handles ${JSON.stringify(q).slice(0, 24)}`, ok: r.status === 200 && !html.includes("<script>alert(1)</script>"), detail: String(r.status) }); const s = await get(`/api/suggest?q=${encodeURIComponent(q)}`); checks.push({ name: `suggest handles ${JSON.stringify(q).slice(0, 24)}`, ok: s.status === 200 || s.status === 400, detail: String(s.status) }); }15  // 4. no private data in public payloads16  { const r = await get("/tools/duckdb"); const html = await r.text(); checks.push({ name: "tool page has no owner/private fields", ok: !/owner_notes|deployment_urls|use_state|subscription_status|"visibility"|totp|password_hash/.test(html) }); }17  { const r = await get("/api/suggest?q=a"); const j = await r.json(); checks.push({ name: "suggest payload only public keys", ok: Array.isArray(j) && j.every((x: any) => Object.keys(x).every((k) => ["slug", "name", "summary", "kind"].includes(k))) }); }18  { const r = await get("/sitemap.xml"); const x = await r.text(); checks.push({ name: "sitemap excludes admin/login", ok: !x.includes("/admin") && !x.includes("/login") }); }19  // 5. security headers20  { const r = await get("/"); const h = r.headers; checks.push({ name: "CSP present", ok: !!h.get("content-security-policy") }); checks.push({ name: "frame-ancestors none", ok: (h.get("content-security-policy") ?? "").includes("frame-ancestors 'none'") }); checks.push({ name: "nosniff", ok: h.get("x-content-type-options") === "nosniff" }); checks.push({ name: "referrer policy", ok: !!h.get("referrer-policy") }); checks.push({ name: "no x-powered-by", ok: !h.get("x-powered-by") }); }21  { const r = await get("/admin"); checks.push({ name: "admin noindex header", ok: (r.headers.get("x-robots-tag") ?? "").includes("noindex") }); }22  // 6. error report abuse: honeypot + size23  { const r = await get("/api/report", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ slug: "duckdb", message: "x".repeat(5000) }) }); checks.push({ name: "report rejects oversize (400) or is rate-limited (429)", ok: r.status === 400 || r.status === 429, detail: String(r.status) }); }24  { const r = await get("/api/report", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ slug: "duckdb", message: "bot filling honeypot field", website: "spam" }) }); checks.push({ name: "report rejects honeypot (400) or is rate-limited (429)", ok: r.status === 400 || r.status === 429, detail: String(r.status) }); }25  // 7. IDOR on tool ids: unauth GET of an admin record by id/slug26  { const r = await get("/api/admin/tools/duckdb"); checks.push({ name: "admin record by slug unauth → 401", ok: r.status === 401, detail: String(r.status) }); }27  // 8. path traversal / weird slugs28  for (const p of ["/tools/..%2F..%2Fetc%2Fpasswd", "/tools/%27", "/categories/<img src=x onerror=alert(1)>", "/collections/' or 1=1"]) { const r = await get(p); checks.push({ name: `hostile path ${p.slice(0, 30)} → 404/400`, ok: r.status === 404 || r.status === 400, detail: String(r.status) }); }29}30await run();31const failed = checks.filter((c) => !c.ok);32for (const c of checks) console.log(`${c.ok ? "PASS" : "FAIL"} ${c.name}${c.detail ? ` (${c.detail})` : ""}`);33console.log(JSON.stringify({ total: checks.length, failed: failed.length, at: new Date().toISOString() }));34if (failed.length) process.exit(1);35export {};36
Files · 26d3764ea3f3414ef122de45da4ad93a2165fb05
Menu

Theme