diff --git a/scripts/check-client-bundle-secrets.mjs b/scripts/check-client-bundle-secrets.mjs index 9319a103f..a007ab333 100644 --- a/scripts/check-client-bundle-secrets.mjs +++ b/scripts/check-client-bundle-secrets.mjs @@ -11,9 +11,12 @@ const forbiddenMarkers = [ "OPENAI_ORG_ID", "OPENAI_PROJECT_ID", "RAG_QUERY_HASH_SECRET", - "sb_secret_", - "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) { @@ -47,9 +50,11 @@ const offenders = new Map(); for (const file of [...textFiles(publicRoot), ...textFiles(clientBuildRoot)]) { const content = readFileSync(file, "utf8"); for (const marker of forbiddenMarkers) { - if (content.includes(marker)) { + 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${marker}`, { marker, 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..8f911214c 100644 --- a/tests/client-secret-surface.test.ts +++ b/tests/client-secret-surface.test.ts @@ -169,6 +169,23 @@ 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")); + writeFileSync(join(staticRoot, "leaked-key.js"), "const key = 'sb_secret_abc123DEF456';", "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 }); }