71 lines
2.6 KiB
TypeScript
71 lines
2.6 KiB
TypeScript
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
|
import { DatabaseTableService } from '../../src/services/database/database-table.service';
|
|
|
|
// Create a single mock query function to track all database calls
|
|
const sharedMockQuery = vi.fn();
|
|
|
|
// Mock DatabaseManager
|
|
vi.mock('../../src/infra/database/database.manager', () => {
|
|
return {
|
|
DatabaseManager: {
|
|
getInstance: vi.fn(() => ({
|
|
getPool: vi.fn(() => ({
|
|
connect: vi.fn().mockResolvedValue({
|
|
query: sharedMockQuery,
|
|
release: vi.fn(),
|
|
}),
|
|
query: sharedMockQuery,
|
|
})),
|
|
})),
|
|
},
|
|
};
|
|
});
|
|
|
|
describe('DatabaseTableService - SQL Injection Verification (Fixed)', () => {
|
|
let service: DatabaseTableService;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
service = DatabaseTableService.getInstance();
|
|
|
|
// Setup sequential responses to satisfy code path logic
|
|
sharedMockQuery
|
|
.mockResolvedValueOnce({
|
|
rows: [{ column_name: 'id', data_type: 'uuid', udt_name: 'uuid', is_nullable: 'NO' }],
|
|
}) // 1. Columns schema
|
|
.mockResolvedValueOnce({ rows: [] }) // 2. Fkey constraints (pooled query)
|
|
.mockResolvedValueOnce({ rows: [{ column_name: 'id' }] }) // 3. Primary keys
|
|
.mockResolvedValueOnce({ rows: [] }) // 4. Unique columns
|
|
.mockResolvedValueOnce({ rows: [{ row_count: 0 }] }); // 5. COUNT(*) query
|
|
});
|
|
|
|
test('getTableSchema correctly escapes table names to prevent SQL injection', async () => {
|
|
// Malicious table name keeps SQL metacharacters but avoids quotes so it reaches the SQL builder.
|
|
const maliciousTableName = 'users; DROP TABLE secrets; --';
|
|
|
|
try {
|
|
// Execute the method
|
|
await service.getTableSchema('public', maliciousTableName);
|
|
} catch {
|
|
// Ignore logical errors, we only care about the captured SQL
|
|
}
|
|
|
|
// Find the binary query that contains our malicious payload (the COUNT query)
|
|
const injectionQueryCall = sharedMockQuery.mock.calls.find(
|
|
(call) => typeof call[0] === 'string' && call[0].includes('COUNT(*)')
|
|
);
|
|
|
|
expect(injectionQueryCall).toBeDefined();
|
|
const capturedQuery = injectionQueryCall![0];
|
|
|
|
console.log('Captured SQL Query:', capturedQuery);
|
|
|
|
// Assert that the table name stays inside a schema-qualified quoted identifier.
|
|
expect(capturedQuery).toContain(`FROM "public"."users; DROP TABLE secrets; --"`);
|
|
|
|
// This proves that the DROP TABLE command is now safely inside the identifier
|
|
// string and will NOT be executed as a separate SQL command.
|
|
expect(capturedQuery).not.toMatch(/FROM "public"\."[^"]*"; DROP TABLE/);
|
|
});
|
|
});
|