Skip to content

Commit fd00960

Browse files
committed
fix(cli): add public schema existence check and null byte test
- Add existence check for public schema before has_schema_privilege (prevents error if public schema is dropped) - Add test for null byte rejection in escapeLiteral via database name
1 parent 528b451 commit fd00960

File tree

2 files changed

+36
-5
lines changed

2 files changed

+36
-5
lines changed

cli/lib/supabase.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -616,13 +616,21 @@ export async function verifyInitSetupViaSupabase(params: {
616616
}
617617
}
618618

619-
// Check USAGE on public schema
620-
const schemaUsageRes = await params.client.query(
621-
`SELECT has_schema_privilege('${escapeLiteral(role)}', 'public', 'USAGE') as ok`,
619+
// Check USAGE on public schema (check existence first to avoid has_schema_privilege throwing)
620+
const publicSchemaExistsRes = await params.client.query(
621+
"SELECT nspname FROM pg_namespace WHERE nspname = 'public'",
622622
true
623623
);
624-
if (!schemaUsageRes.rows?.[0]?.ok) {
625-
missingRequired.push("USAGE on schema public");
624+
if (publicSchemaExistsRes.rowCount === 0) {
625+
missingRequired.push("schema public exists");
626+
} else {
627+
const schemaUsageRes = await params.client.query(
628+
`SELECT has_schema_privilege('${escapeLiteral(role)}', 'public', 'USAGE') as ok`,
629+
true
630+
);
631+
if (!schemaUsageRes.rows?.[0]?.ok) {
632+
missingRequired.push("USAGE on schema public");
633+
}
626634
}
627635

628636
// Check search_path

cli/test/supabase.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,29 @@ describe("Supabase module", () => {
521521
}
522522
});
523523

524+
test("throws error for database name with null bytes (SQL injection prevention)", async () => {
525+
globalThis.fetch = mock(() =>
526+
Promise.resolve(new Response(JSON.stringify([{ rolname: "postgres_ai_mon" }]), { status: 200 }))
527+
) as unknown as typeof fetch;
528+
529+
const client = new SupabaseClient({
530+
projectRef: VALID_PROJECT_REF,
531+
accessToken: "mytoken",
532+
});
533+
534+
try {
535+
await verifyInitSetupViaSupabase({
536+
client,
537+
database: "test\0db", // null byte should be rejected
538+
monitoringUser: "postgres_ai_mon",
539+
includeOptionalPermissions: false,
540+
});
541+
throw new Error("Expected to throw");
542+
} catch (e) {
543+
expect((e as Error).message).toContain("null bytes");
544+
}
545+
});
546+
524547
test("returns missing role when role does not exist", async () => {
525548
globalThis.fetch = mock(() =>
526549
Promise.resolve(new Response(JSON.stringify([]), { status: 200 }))

0 commit comments

Comments
 (0)