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:
+39
-4
@@ -53,16 +53,29 @@ let mockJwks = null;
|
||||
let cachedJwks = null; // OIDC public keys
|
||||
|
||||
// ── initAuth ────────────────────────────────────────────────────────────────
|
||||
function initAuth() {
|
||||
async function initAuth() {
|
||||
// Читаем здесь — не на уровне модуля, чтобы тесты могли менять process.env
|
||||
// до вызова initAuth() и видеть актуальные значения.
|
||||
const devMode = process.env.DEV_MODE === 'true';
|
||||
const devSecret = process.env.DEV_SECRET || '';
|
||||
|
||||
// DEV_MODE=true недопустим в production — жёсткая защита от случайного деплоя.
|
||||
if (process.env.NODE_ENV === 'production' && devMode) {
|
||||
throw new Error('DEV_MODE=true запрещён в production (NODE_ENV=production)');
|
||||
}
|
||||
|
||||
const isOidc = !!(KC_CLIENT_ID && KC_CLIENT_SECRET);
|
||||
|
||||
if (isOidc) {
|
||||
console.log('[auth] OIDC — Keycloak', KC_BASE_URL);
|
||||
// Предзагрузка JWKS при старте — избегаем задержки на первом запросе.
|
||||
// При ошибке логируем предупреждение: JWKS будет загружен в exchangeCode().
|
||||
try {
|
||||
cachedJwks = JSON.parse(await httpGet(OIDC.certs));
|
||||
console.log('[auth] JWKS loaded:', cachedJwks.keys.length, 'ключей');
|
||||
} catch (e) {
|
||||
console.warn('[auth] JWKS preload failed:', e.message, '— будет загружен при первом запросе');
|
||||
}
|
||||
} else {
|
||||
console.log('[auth] MOCK — local RS256 key');
|
||||
mockKeyPair = crypto.generateKeyPairSync('rsa', {
|
||||
@@ -165,6 +178,7 @@ async function exchangeCode(code) {
|
||||
const data = JSON.parse(raw);
|
||||
if (!data.access_token) throw new Error('No access_token: ' + raw.slice(0, 200));
|
||||
|
||||
// cachedJwks загружается в initAuth(); при провале preload — подгружаем здесь.
|
||||
if (!cachedJwks) cachedJwks = JSON.parse(await httpGet(OIDC.certs));
|
||||
const payload = verifyOidcToken(data.access_token);
|
||||
|
||||
@@ -200,10 +214,31 @@ function verifyMockToken(token) {
|
||||
|
||||
function verifyOidcToken(token) {
|
||||
if (!cachedJwks) throw new Error('JWKS not loaded yet');
|
||||
const key = cachedJwks.keys.find(k => k.kty === 'RSA' && k.use === 'sig');
|
||||
if (!key) throw new Error('No RSA sig key in JWKS');
|
||||
|
||||
// Декодируем заголовок токена для выбора ключа по kid.
|
||||
// base64url безопаснее base64: обрабатывает '-' и '_' без паддинга.
|
||||
const headerRaw = token.split('.')[0];
|
||||
let header;
|
||||
try {
|
||||
header = JSON.parse(Buffer.from(headerRaw, 'base64url').toString());
|
||||
} catch (e) {
|
||||
throw new Error('Malformed JWT header');
|
||||
}
|
||||
const kid = header.kid;
|
||||
|
||||
// Ищем ключ по kid (точный match) или первый RSA-sig ключ (если kid не указан).
|
||||
const key = kid
|
||||
? cachedJwks.keys.find(k => k.kid === kid && k.kty === 'RSA' && k.use === 'sig')
|
||||
: cachedJwks.keys.find(k => k.kty === 'RSA' && k.use === 'sig');
|
||||
if (!key) throw new Error('No matching RSA sig key in JWKS' + (kid ? ` (kid=${kid})` : ''));
|
||||
|
||||
const pem = crypto.createPublicKey({ key, format: 'jwk' }).export({ type: 'spki', format: 'pem' });
|
||||
return jwt.verify(token, pem, { algorithms: ['RS256'] });
|
||||
// Проверяем issuer и audience — защита от токенов других realm/клиентов (A02).
|
||||
return jwt.verify(token, pem, {
|
||||
algorithms: ['RS256'],
|
||||
issuer: KC_BASE_URL,
|
||||
audience: KC_CLIENT_ID,
|
||||
});
|
||||
}
|
||||
|
||||
function userFromPayload(payload) {
|
||||
|
||||
Reference in New Issue
Block a user