File size: 5,995 Bytes
ec1b111 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { buildApp } from '../src/app';
// βββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function makeJwt(app: any, payload: object) {
return (app as any).jwt.sign(payload);
}
// βββ Suite ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
describe('Security β SQL Injection & Role Guards', () => {
let app: any;
beforeAll(async () => {
app = await buildApp();
});
afterAll(async () => {
await app.close();
});
// ββ Analytics text-to-SQL injection protection (pure logic) βββββββββββββ
// HTTP integration tests for analytics/query require a real DB org (the
// injectTenantConfig middleware runs before the route). Pattern validation
// is tested here as pure unit checks.
describe('SQL injection DANGEROUS_PATTERNS β unit checks', () => {
// Mirror the exact patterns from analytics.ts
const DANGEROUS_PATTERNS = [/\bUNION\b/, /\bINSERT\b/, /\bUPDATE\b/, /\bDELETE\b/, /\bDROP\b/, /\bEXEC\b/, /\bEXECUTE\b/, /--/, /\/\*/, /;\s*SELECT/i];
it('blocks UNION SELECT', () => {
const sql = "SELECT * FROM \"User\" WHERE \"organizationId\" = 'x' UNION SELECT * FROM \"User\"";
expect(DANGEROUS_PATTERNS.some(p => p.test(sql.toUpperCase()))).toBe(true);
});
it('blocks DROP TABLE', () => {
expect(DANGEROUS_PATTERNS.some(p => p.test('DROP TABLE "User"'.toUpperCase()))).toBe(true);
});
it('blocks -- SQL comment', () => {
expect(DANGEROUS_PATTERNS.some(p => p.test('SELECT 1 -- bypass filter'))).toBe(true);
});
it('blocks /* block comment */', () => {
expect(DANGEROUS_PATTERNS.some(p => p.test('SELECT 1 /* comment */'))).toBe(true);
});
it('blocks DELETE', () => {
expect(DANGEROUS_PATTERNS.some(p => p.test('DELETE FROM "User" WHERE 1=1'.toUpperCase()))).toBe(true);
});
it('blocks INSERT', () => {
expect(DANGEROUS_PATTERNS.some(p => p.test('INSERT INTO "User" VALUES (1)'.toUpperCase()))).toBe(true);
});
it('blocks UPDATE', () => {
expect(DANGEROUS_PATTERNS.some(p => p.test('UPDATE "User" SET name=\'x\''.toUpperCase()))).toBe(true);
});
it('blocks stacked query with ; SELECT', () => {
expect(DANGEROUS_PATTERNS.some(p => p.test('SELECT 1; SELECT * FROM "User"'))).toBe(true);
});
it('allows safe SELECT without dangerous patterns', () => {
const sql = 'SELECT id, name FROM "User" WHERE "organizationId" = \'abc\' LIMIT 10';
expect(DANGEROUS_PATTERNS.some(p => p.test(sql.toUpperCase()))).toBe(false);
});
it('allows SELECT with GROUP BY and ORDER BY', () => {
const sql = 'SELECT feature, COUNT(*) as total FROM "UsageEvent" WHERE "organizationId" = \'abc\' GROUP BY feature ORDER BY total DESC LIMIT 5';
expect(DANGEROUS_PATTERNS.some(p => p.test(sql.toUpperCase()))).toBe(false);
});
});
// ββ Admin route unauthenticated βββββββββββββββββββββββββββββββββββββββββββ
// Role guard (STUDENT/ORG_MEMBER β 403) requires a real DB org record because
// injectTenantConfig runs before the route-level role check. The integration
// tests in critical-flows.test.ts cover the authenticated path.
describe('GET /v1/admin/stats β auth check', () => {
it('returns 401 with no token', async () => {
const res = await app.inject({ method: 'GET', url: '/v1/admin/stats' });
expect(res.statusCode).toBe(401);
});
});
// ββ Super-admin route authentication & authorization βββββββββββββββββββββ
describe('GET /v1/super-admin/platform/stats β auth & role', () => {
it('returns 401 with no token', async () => {
const res = await app.inject({ method: 'GET', url: '/v1/super-admin/platform/stats' });
expect(res.statusCode).toBe(401);
});
it('returns 403 for ORG_ADMIN role', async () => {
const token = makeJwt(app, { id: 'u1', role: 'ORG_ADMIN', organizationId: 'org1' });
const res = await app.inject({
method: 'GET',
url: '/v1/super-admin/platform/stats',
headers: { Authorization: `Bearer ${token}` },
});
expect(res.statusCode).toBe(403);
});
it('returns 403 for STUDENT role', async () => {
const token = makeJwt(app, { id: 'u1', role: 'STUDENT', organizationId: 'org1' });
const res = await app.inject({
method: 'GET',
url: '/v1/super-admin/platform/stats',
headers: { Authorization: `Bearer ${token}` },
});
expect(res.statusCode).toBe(403);
});
});
// ββ JWT secret enforcement ββββββββββββββββββββββββββββββββββββββββββββββββ
describe('JWT tampered token', () => {
it('returns 401 for a tampered JWT', async () => {
const res = await app.inject({
method: 'GET',
url: '/v1/admin/stats',
headers: { Authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImZha2UifQ.invalidsignature' },
});
expect(res.statusCode).toBe(401);
});
});
});
|