168 lines
6.0 KiB
TypeScript
168 lines
6.0 KiB
TypeScript
import { beforeAll, describe, it, expect } from 'vitest';
|
|
import {
|
|
checkSqlExecutionGuards,
|
|
initSqlParser,
|
|
parseSQLStatements,
|
|
} from '../../src/utils/sql-parser';
|
|
|
|
beforeAll(async () => {
|
|
await initSqlParser();
|
|
});
|
|
|
|
describe('parseSQLStatements', () => {
|
|
it('splits multiple statements by semicolon', () => {
|
|
const sql = `
|
|
SELECT * FROM users;
|
|
INSERT INTO users (name) VALUES ('John');
|
|
DELETE FROM users WHERE id = 1;
|
|
`;
|
|
const result = parseSQLStatements(sql);
|
|
expect(result).toEqual([
|
|
'SELECT * FROM users',
|
|
"INSERT INTO users (name) VALUES ('John')",
|
|
'DELETE FROM users WHERE id = 1',
|
|
]);
|
|
});
|
|
|
|
it('ignores line comments', () => {
|
|
const sql = `
|
|
-- This is a comment
|
|
SELECT * FROM users; -- Inline comment
|
|
`;
|
|
const result = parseSQLStatements(sql);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toContain('SELECT * FROM users');
|
|
});
|
|
|
|
it('ignores block comments', () => {
|
|
const sql = `
|
|
/* Block comment */
|
|
SELECT * FROM users;
|
|
/* Another comment */
|
|
`;
|
|
const result = parseSQLStatements(sql);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toContain('SELECT * FROM users');
|
|
});
|
|
|
|
it('handles semicolons inside string literals', () => {
|
|
const sql = `INSERT INTO messages (text) VALUES ('Hello; World')`;
|
|
const result = parseSQLStatements(sql);
|
|
expect(result).toEqual([`INSERT INTO messages (text) VALUES ('Hello; World')`]);
|
|
});
|
|
|
|
it('throws error on empty input', () => {
|
|
expect(() => parseSQLStatements('')).toThrow();
|
|
});
|
|
|
|
it('returns empty array for comments-only SQL', () => {
|
|
const sql = `
|
|
-- Only comment
|
|
/* Another comment */
|
|
`;
|
|
const result = parseSQLStatements(sql);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('trims statements and removes empty results', () => {
|
|
const sql = `
|
|
SELECT * FROM users;
|
|
-- comment
|
|
INSERT INTO users (id) VALUES (1);
|
|
`;
|
|
const result = parseSQLStatements(sql);
|
|
expect(result.length).toBeGreaterThan(0);
|
|
expect(result[0]).toContain('SELECT * FROM users');
|
|
expect(result[result.length - 1] || result[0]).toContain('INSERT INTO users');
|
|
});
|
|
});
|
|
|
|
describe('SQL execution guards', () => {
|
|
it('blocks role and session authorization changes', () => {
|
|
expect(checkSqlExecutionGuards('SET ROLE postgres')).not.toBeNull();
|
|
expect(checkSqlExecutionGuards('SET LOCAL ROLE postgres')).not.toBeNull();
|
|
expect(checkSqlExecutionGuards('RESET ROLE')).not.toBeNull();
|
|
expect(checkSqlExecutionGuards('SET SESSION AUTHORIZATION postgres')).not.toBeNull();
|
|
expect(checkSqlExecutionGuards('RESET SESSION AUTHORIZATION')).not.toBeNull();
|
|
expect(checkSqlExecutionGuards("SELECT set_config('role', 'postgres', false)")).not.toBeNull();
|
|
expect(
|
|
checkSqlExecutionGuards("SELECT set_config('role'::text, 'postgres', false)")
|
|
).not.toBeNull();
|
|
expect(checkSqlExecutionGuards("SELECT set_config($1, 'postgres', false)")).not.toBeNull();
|
|
expect(
|
|
checkSqlExecutionGuards("SELECT set_config('ro' || 'le', 'postgres', false)")
|
|
).not.toBeNull();
|
|
expect(
|
|
checkSqlExecutionGuards("SELECT pg_catalog.set_config('role', 'postgres', false)")
|
|
).not.toBeNull();
|
|
expect(
|
|
checkSqlExecutionGuards("SELECT pg_catalog.\"set_config\"('role', 'postgres', false)")
|
|
).not.toBeNull();
|
|
expect(checkSqlExecutionGuards("SELECT set_config('app.safe', 'value', false)")).not.toBeNull();
|
|
});
|
|
|
|
it('blocks role management statements but allows object grants', () => {
|
|
expect(checkSqlExecutionGuards('CREATE ROLE app_owner')).not.toBeNull();
|
|
expect(
|
|
checkSqlExecutionGuards('ALTER ROLE project_admin SET search_path TO public')
|
|
).not.toBeNull();
|
|
expect(checkSqlExecutionGuards('DROP ROLE app_owner')).not.toBeNull();
|
|
expect(checkSqlExecutionGuards('GRANT authenticated TO project_admin')).not.toBeNull();
|
|
expect(checkSqlExecutionGuards('GRANT SELECT ON public.todos TO authenticated')).toBeNull();
|
|
});
|
|
|
|
it('blocks transaction control statements', () => {
|
|
expect(checkSqlExecutionGuards('BEGIN')).not.toBeNull();
|
|
expect(checkSqlExecutionGuards('COMMIT')).not.toBeNull();
|
|
expect(checkSqlExecutionGuards('ROLLBACK')).not.toBeNull();
|
|
expect(checkSqlExecutionGuards('SELECT 1')).toBeNull();
|
|
});
|
|
|
|
it('blocks search_path changes without matching comments or strings', () => {
|
|
expect(checkSqlExecutionGuards('SET search_path TO public')).not.toBeNull();
|
|
expect(
|
|
checkSqlExecutionGuards("SELECT set_config('search_path', 'public', false)")
|
|
).not.toBeNull();
|
|
expect(
|
|
checkSqlExecutionGuards("SELECT set_config('search_path'::text, 'public', false)")
|
|
).not.toBeNull();
|
|
expect(checkSqlExecutionGuards("SELECT 'SET search_path TO public'")).toBeNull();
|
|
expect(checkSqlExecutionGuards('-- SET search_path TO public\nSELECT 1')).toBeNull();
|
|
});
|
|
|
|
it('blocks reset-all and statement timeout changes', () => {
|
|
expect(checkSqlExecutionGuards('RESET ALL')).not.toBeNull();
|
|
expect(checkSqlExecutionGuards('SET statement_timeout = 0')).not.toBeNull();
|
|
expect(checkSqlExecutionGuards('RESET statement_timeout')).not.toBeNull();
|
|
expect(
|
|
checkSqlExecutionGuards("SELECT set_config('statement_timeout', '0', false)")
|
|
).not.toBeNull();
|
|
});
|
|
|
|
it('blocks set_config inside DO blocks without blocking ordinary DO blocks', () => {
|
|
expect(
|
|
checkSqlExecutionGuards(`
|
|
DO $$
|
|
BEGIN
|
|
PERFORM set_config('role', 'postgres', false);
|
|
END $$;
|
|
`)
|
|
).not.toBeNull();
|
|
|
|
expect(
|
|
checkSqlExecutionGuards(`
|
|
DO $$
|
|
BEGIN
|
|
RAISE NOTICE 'hello';
|
|
END $$;
|
|
`)
|
|
).toBeNull();
|
|
});
|
|
|
|
it('blocks database-level operations without matching comments or strings', () => {
|
|
expect(checkSqlExecutionGuards('DROP DATABASE customer_project')).not.toBeNull();
|
|
expect(checkSqlExecutionGuards("SELECT 'DROP DATABASE customer_project'")).toBeNull();
|
|
expect(checkSqlExecutionGuards('-- DROP DATABASE customer_project\nSELECT 1')).toBeNull();
|
|
});
|
|
});
|