Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
176 lines
5.6 KiB
JavaScript
176 lines
5.6 KiB
JavaScript
// backend/utils/__tests__/sqlSanitizer.test.js
|
|
// SEC-001: SQL Sanitizer Tests - ADR-015
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
validateTableName,
|
|
escapeIdentifier,
|
|
buildWhereClause,
|
|
buildInClause,
|
|
ALLOWED_TABLES
|
|
} from '../sqlSanitizer.js';
|
|
|
|
describe('SQL Sanitizer', () => {
|
|
describe('validateTableName', () => {
|
|
it('should accept valid table names', () => {
|
|
const validTables = [
|
|
'users',
|
|
'spaces',
|
|
'projects',
|
|
'dashboards',
|
|
'universal_tables',
|
|
'table_columns',
|
|
'table_rows',
|
|
'widgets',
|
|
'api_keys',
|
|
'webhooks',
|
|
'webhook_logs',
|
|
'files',
|
|
'folders',
|
|
'chat_threads',
|
|
'chat_participants',
|
|
'chat_messages',
|
|
'audit_log',
|
|
'system_settings',
|
|
'user_settings',
|
|
'data_sources',
|
|
'sync_logs',
|
|
'schema_layouts',
|
|
'monitoring_runs',
|
|
'monitoring_events'
|
|
];
|
|
|
|
for (const table of validTables) {
|
|
expect(() => validateTableName(table)).not.toThrow();
|
|
expect(validateTableName(table)).toBe(table);
|
|
}
|
|
});
|
|
|
|
it('should reject invalid table names', () => {
|
|
const invalidTables = [
|
|
'nonexistent_table',
|
|
'users; DROP TABLE users;--',
|
|
"users' OR '1'='1",
|
|
'users UNION SELECT * FROM passwords',
|
|
'../../../etc/passwd',
|
|
'',
|
|
null,
|
|
undefined,
|
|
123,
|
|
'SELECT * FROM users',
|
|
'users--',
|
|
'users/*',
|
|
'table"name'
|
|
];
|
|
|
|
for (const table of invalidTables) {
|
|
expect(() => validateTableName(table)).toThrow(/Invalid table name/);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('escapeIdentifier', () => {
|
|
it('should escape valid column/table names', () => {
|
|
expect(escapeIdentifier('column_name')).toBe('"column_name"');
|
|
expect(escapeIdentifier('TableName')).toBe('"TableName"');
|
|
expect(escapeIdentifier('_private')).toBe('"_private"');
|
|
expect(escapeIdentifier('column123')).toBe('"column123"');
|
|
});
|
|
|
|
it('should escape double quotes in identifiers', () => {
|
|
expect(escapeIdentifier('col"name')).toBe('"col""name"');
|
|
expect(escapeIdentifier('a"b"c')).toBe('"a""b""c"');
|
|
});
|
|
|
|
it('should reject dangerous identifiers', () => {
|
|
expect(() => escapeIdentifier('col; DROP TABLE')).toThrow();
|
|
expect(() => escapeIdentifier("col' OR '1'='1")).toThrow();
|
|
expect(() => escapeIdentifier('col--comment')).toThrow();
|
|
expect(() => escapeIdentifier('123start')).toThrow();
|
|
expect(() => escapeIdentifier('')).toThrow();
|
|
expect(() => escapeIdentifier(null)).toThrow();
|
|
expect(() => escapeIdentifier(undefined)).toThrow();
|
|
expect(() => escapeIdentifier('col/*comment*/')).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('buildWhereClause', () => {
|
|
it('should build parameterized WHERE clause', () => {
|
|
const filters = { status: 'active', type: 'user' };
|
|
const { clause, params } = buildWhereClause(filters, ['status', 'type']);
|
|
|
|
expect(clause).toBe('WHERE "status" = ? AND "type" = ?');
|
|
expect(params).toEqual(['active', 'user']);
|
|
});
|
|
|
|
it('should handle empty filters', () => {
|
|
const { clause, params } = buildWhereClause({}, ['status']);
|
|
|
|
expect(clause).toBe('');
|
|
expect(params).toEqual([]);
|
|
});
|
|
|
|
it('should ignore non-whitelisted fields', () => {
|
|
const filters = { status: 'active', injected: 'DROP TABLE users' };
|
|
const { clause, params } = buildWhereClause(filters, ['status']);
|
|
|
|
expect(clause).toBe('WHERE "status" = ?');
|
|
expect(params).toEqual(['active']);
|
|
expect(params).not.toContain('DROP TABLE users');
|
|
});
|
|
|
|
it('should handle null and undefined values', () => {
|
|
const filters = { status: 'active', type: null, name: undefined };
|
|
const { clause, params } = buildWhereClause(filters, ['status', 'type', 'name']);
|
|
|
|
expect(clause).toBe('WHERE "status" = ?');
|
|
expect(params).toEqual(['active']);
|
|
});
|
|
|
|
it('should handle null/undefined filters object', () => {
|
|
expect(buildWhereClause(null, ['status'])).toEqual({ clause: '', params: [] });
|
|
expect(buildWhereClause(undefined, ['status'])).toEqual({ clause: '', params: [] });
|
|
});
|
|
});
|
|
|
|
describe('buildInClause', () => {
|
|
it('should build IN clause with placeholders', () => {
|
|
const { clause, params } = buildInClause('id', [1, 2, 3]);
|
|
|
|
expect(clause).toBe('"id" IN (?, ?, ?)');
|
|
expect(params).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
it('should handle single value', () => {
|
|
const { clause, params } = buildInClause('status', ['active']);
|
|
|
|
expect(clause).toBe('"status" IN (?)');
|
|
expect(params).toEqual(['active']);
|
|
});
|
|
|
|
it('should reject empty array', () => {
|
|
expect(() => buildInClause('id', [])).toThrow('Values must be a non-empty array');
|
|
});
|
|
|
|
it('should reject non-array values', () => {
|
|
expect(() => buildInClause('id', null)).toThrow();
|
|
expect(() => buildInClause('id', 'value')).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('ALLOWED_TABLES constant', () => {
|
|
it('should be a Set', () => {
|
|
expect(ALLOWED_TABLES).toBeInstanceOf(Set);
|
|
});
|
|
|
|
it('should contain core tables', () => {
|
|
expect(ALLOWED_TABLES.has('users')).toBe(true);
|
|
expect(ALLOWED_TABLES.has('spaces')).toBe(true);
|
|
expect(ALLOWED_TABLES.has('projects')).toBe(true);
|
|
});
|
|
|
|
it('should not contain dangerous values', () => {
|
|
expect(ALLOWED_TABLES.has('DROP')).toBe(false);
|
|
expect(ALLOWED_TABLES.has('; --')).toBe(false);
|
|
});
|
|
});
|
|
});
|