security: fix 7 vulnerabilities from Opus code review
- [A01] /export: mount after auth.middleware, filter by company for non-admin - [A02] OIDC: verify issuer, audience, select key by kid; preload JWKS at startup - [A07] session fixation: session.regenerate() in POST /login, GET /callback, POST /dev-login - [A10] open redirect: safeReturn() helper validates returnTo (blocks //evil.com, https:// etc) - [A05] CSRF: getSessionIdentifier uses req.sessionID instead of dead req.cookies.jwt - [A05] fail-fast: reject default SESSION_SECRET / CSRF_SECRET in NODE_ENV=production - [A05] DEV_MODE=true blocked in production Tests: 68 PASS, 0 FAIL (was 50, added 18 new security-focused tests)
This commit is contained in:
+102
-3
@@ -127,17 +127,22 @@ test('overlap exact match', overlaps('192.168.0.0/24', '192.168.0.0/24'), true);
|
||||
test('no overlap public', overlaps('8.8.8.0/24', '9.9.9.0/24'), false);
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// 4. src/auth.js — session middleware (без JWT, без БД)
|
||||
// 4. src/auth.js — session middleware (async, без JWT, без БД)
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
section('auth.js (session middleware)');
|
||||
|
||||
process.env.DEV_MODE = 'true';
|
||||
process.env.ADMIN_CLIENT_ID = 'WZ01112';
|
||||
|
||||
const { initAuth, requireAdmin } = require('../src/auth');
|
||||
const { safeReturn } = require('../src/routes/auth');
|
||||
const csrfMiddleware = require('../src/middleware/csrf');
|
||||
|
||||
(async () => {
|
||||
|
||||
section('auth.js (session middleware)');
|
||||
|
||||
try {
|
||||
const auth = initAuth();
|
||||
const auth = await initAuth();
|
||||
test('middleware is function', typeof auth.middleware, 'function');
|
||||
test('jwksHandler present (mock mode)', typeof auth.jwksHandler, 'function');
|
||||
test('devLoginEnabled when DEV_MODE', auth.devLoginEnabled, true);
|
||||
@@ -183,6 +188,95 @@ try {
|
||||
fail++;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// 5. safeReturn — защита от open redirect (A01)
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
section('safeReturn (open redirect protection)');
|
||||
|
||||
test('normal path allowed', safeReturn('/dashboard'), '/dashboard');
|
||||
test('root / allowed', safeReturn('/'), '/');
|
||||
test('path with query allowed', safeReturn('/edit?id=5'), '/edit?id=5');
|
||||
test('//evil.com blocked', safeReturn('//evil.com'), '/');
|
||||
test('https://evil.com blocked', safeReturn('https://evil.com'), '/');
|
||||
test('http://evil.com blocked', safeReturn('http://evil.com'), '/');
|
||||
test('undefined → /', safeReturn(undefined), '/');
|
||||
test('null → /', safeReturn(null), '/');
|
||||
test('empty string → /', safeReturn(''), '/');
|
||||
test('//ok.com/path blocked', safeReturn('//ok.com/path'), '/');
|
||||
test('nested path allowed', safeReturn('/admin/users'), '/admin/users');
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// 6. CSRF middleware — getSessionIdentifier использует req.sessionID
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
section('CSRF getSessionIdentifier');
|
||||
|
||||
// Проверяем что csrf.js экспортирует объект с initCsrf
|
||||
test('csrf module exports initCsrf', typeof csrfMiddleware.initCsrf, 'function');
|
||||
|
||||
// Проверяем поведение getSessionIdentifier через тестовые значения
|
||||
// Функция в модуле читает req.sessionID (не req.cookies.jwt)
|
||||
const { doubleCsrfProtection: dp, generateCsrfToken: gct } = csrfMiddleware.initCsrf();
|
||||
test('initCsrf returns doubleCsrfProtection fn', typeof dp, 'function');
|
||||
test('initCsrf returns generateCsrfToken fn', typeof gct, 'function');
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// 7. Export auth isolation — req.user влияет на фильтр компании
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
section('Export auth isolation');
|
||||
|
||||
// Тестируем логику без реального HTTP — проверяем что createRouter не бросает при require
|
||||
const exportModule = require('../src/routes/export');
|
||||
test('export module exports createRouter', typeof exportModule.createRouter, 'function');
|
||||
|
||||
// Логика изоляции: обычный пользователь получает companyId из q.getOrCreateCompany,
|
||||
// admin без ?company → companyId = null (все компании).
|
||||
// Проверяем через минимальный stub — без реальной БД.
|
||||
let exportCompanyIdCaptured = null;
|
||||
const stubQ = {
|
||||
getOrCreateCompany: async (clientId) => ({ id: 42, clientId }),
|
||||
getExportCIDRs: async (cid) => { exportCompanyIdCaptured = cid; return []; },
|
||||
};
|
||||
const exportRouter = exportModule.createRouter({
|
||||
q: stubQ,
|
||||
exportLimiter: (req, res, next) => next(),
|
||||
aggregateCIDRs: (x) => x,
|
||||
});
|
||||
|
||||
// Симулируем GET /export для обычного пользователя
|
||||
const mockReqUser = {
|
||||
user: { isAdmin: false, clientId: 'WZ01325', companyName: 'TestCo' },
|
||||
query: {},
|
||||
method: 'GET',
|
||||
path: '/export',
|
||||
};
|
||||
const mockResUser = {
|
||||
setHeader: () => {},
|
||||
send: () => {},
|
||||
};
|
||||
// Найдём обработчик /export в router.stack
|
||||
const exportLayer = exportRouter.stack.find(l => l.route && l.route.path === '/export');
|
||||
test('export route registered', !!exportLayer, true);
|
||||
|
||||
if (exportLayer) {
|
||||
// Запускаем handler напрямую
|
||||
const handlers = exportLayer.route.stack.map(l => l.handle);
|
||||
// exportLimiter (stub пропускает) + async handler
|
||||
exportCompanyIdCaptured = 'NOT_CALLED';
|
||||
await handlers[handlers.length - 1](mockReqUser, mockResUser, () => {});
|
||||
test('user export: companyId filtered (42)', exportCompanyIdCaptured, 42);
|
||||
|
||||
// Симулируем GET /export для admin без ?company
|
||||
exportCompanyIdCaptured = 'NOT_CALLED';
|
||||
const mockReqAdmin = {
|
||||
user: { isAdmin: true, clientId: 'WZ01112', companyName: 'Admin' },
|
||||
query: {},
|
||||
method: 'GET',
|
||||
path: '/export',
|
||||
};
|
||||
await handlers[handlers.length - 1](mockReqAdmin, mockResUser, () => {});
|
||||
test('admin export: no filter (null)', exportCompanyIdCaptured, null);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Итог
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
@@ -190,3 +284,8 @@ console.log('\n═════════════════════
|
||||
console.log(pass + ' PASS, ' + fail + ' FAIL');
|
||||
console.log('══════════════════════════════');
|
||||
process.exit(fail > 0 ? 1 : 0);
|
||||
|
||||
})().catch(e => {
|
||||
console.error('\nTest runner fatal error:', e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user