diff --git a/package-lock.json b/package-lock.json index 63c2c05ae..a7478da58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@next/env": "16.2.10", "@supabase/ssr": "^0.12.3", - "@supabase/supabase-js": "^2.108.2", + "@supabase/supabase-js": "^2.110.7", "exceljs": "^4.4.0", "jszip": "^3.10.1", "lucide-react": "^1.22.0", diff --git a/package.json b/package.json index 0c5c5494b..a21d8061c 100644 --- a/package.json +++ b/package.json @@ -178,7 +178,7 @@ "dependencies": { "@next/env": "16.2.10", "@supabase/ssr": "^0.12.3", - "@supabase/supabase-js": "^2.108.2", + "@supabase/supabase-js": "^2.110.7", "exceljs": "^4.4.0", "jszip": "^3.10.1", "lucide-react": "^1.22.0", diff --git a/scripts/check-client-bundle-secrets.mjs b/scripts/check-client-bundle-secrets.mjs index 3e36b1503..a007ab333 100644 --- a/scripts/check-client-bundle-secrets.mjs +++ b/scripts/check-client-bundle-secrets.mjs @@ -11,13 +11,12 @@ const forbiddenMarkers = [ "OPENAI_ORG_ID", "OPENAI_PROJECT_ID", "RAG_QUERY_HASH_SECRET", - // Match an actual secret *value* (prefix + a long token), not the bare "sb_secret_" - // prefix string. @supabase/supabase-js's own browser client code legitimately ships - // that literal prefix (isNewApiKey()/checkApiKeyFormat() in its fetch helpers, used to - // detect new-format Supabase API keys) — that string alone is not a leaked secret. - /\bsb_secret_[A-Za-z0-9_-]{20,}\b/, - "sk-proj-", - "sk-svcacct-", + // Match an actual key value (prefix + key body), not a bare prefix-check + // string literal. @supabase/supabase-js ships `key.startsWith('sb_secret_')` + // client-side as of 2.110.x, which would otherwise be a false positive here. + /sb_secret_[A-Za-z0-9]/, + /sk-proj-[A-Za-z0-9]/, + /sk-svcacct-[A-Za-z0-9]/, ]; function textFiles(root) { @@ -51,11 +50,11 @@ const offenders = new Map(); for (const file of [...textFiles(publicRoot), ...textFiles(clientBuildRoot)]) { const content = readFileSync(file, "utf8"); for (const marker of forbiddenMarkers) { - const matchedText = - marker instanceof RegExp ? content.match(marker)?.[0] : content.includes(marker) ? marker : null; - if (matchedText) { + const matched = marker instanceof RegExp ? marker.test(content) : content.includes(marker); + if (matched) { + const markerLabel = marker instanceof RegExp ? marker.source : marker; const relativePath = relative(projectRoot, file).replaceAll("\\", "/"); - offenders.set(`${relativePath}\0${matchedText}`, { marker: matchedText, relativePath }); + offenders.set(`${relativePath}\0${markerLabel}`, { marker: markerLabel, relativePath }); } } } diff --git a/tests/client-secret-surface.test.ts b/tests/client-secret-surface.test.ts index 9db8dcf02..6274bac46 100644 --- a/tests/client-secret-surface.test.ts +++ b/tests/client-secret-surface.test.ts @@ -169,6 +169,27 @@ describe("client environment isolation", () => { expect(publicResult.status).toBe(1); expect(publicResult.stderr).toContain("public/unsafe.txt"); expect(publicResult.stderr).toContain("SUPABASE_SERVICE_ROLE_KEY"); + + rmSync(join(fixtureRoot, "public", "unsafe.txt")); + // @supabase/supabase-js ships a bare `key.startsWith('sb_secret_')` prefix + // check client-side; that literal alone must not trip the scanner. + writeFileSync( + join(staticRoot, "sdk-prefix-check.js"), + 'const isNewApiKey = (key) => key.startsWith("sb_publishable_") || key.startsWith("sb_secret_");', + "utf8", + ); + const prefixOnlyResult = spawnSync(process.execPath, [scannerPath], { cwd: fixtureRoot, encoding: "utf8" }); + expect(prefixOnlyResult.status).toBe(0); + + rmSync(join(staticRoot, "sdk-prefix-check.js")); + // Built via concatenation so this fixture's synthetic, non-functional key + // never appears as a contiguous literal in this test's own source (which + // would otherwise look like a real leaked secret to git secret scanners). + const fakeSecretKey = ["sb_secret_", "abc123DEF456"].join(""); + writeFileSync(join(staticRoot, "leaked-key.js"), `const key = '${fakeSecretKey}';`, "utf8"); + const leakedKeyResult = spawnSync(process.execPath, [scannerPath], { cwd: fixtureRoot, encoding: "utf8" }); + expect(leakedKeyResult.status).toBe(1); + expect(leakedKeyResult.stderr).toContain(".next/static/leaked-key.js"); } finally { rmSync(fixtureRoot, { recursive: true, force: true }); }