- server.js: санитизация filename в /export (header injection) - ui/api-client.js: req.setTimeout(15000) + try/catch JSON.parse - docs/devops-deploy.md: убран дубль X-Forwarded-Proto
9.1 KiB
Аудит ipwhitelist-app — Claude Sonnet 4.6
Дата: 2026-06-04
Файлы проверены: server.js, src/auth.js, src/routes/oidc.js, src/api/routes/entries.js, ui/routes/auth.js, src/config.js, ui/routes/entries.js, ui/api-client.js, views/index.ejs, .env.example, docs/devops-deploy.md
Вердикт
С оговорками — к деплою почти готов, но есть один critical баг (header injection) и один major баг (зависающие запросы). После исправления этих двух пунктов — production ready.
Проблемы
🔴 CRITICAL
1. HTTP header injection в /export через параметр filename
server.js, строки ~87-91:
const fname = req.query.filename || 'white-list.txt';
res.set('Content-Disposition', disp + '; filename="' + fname + '"');
Если filename содержит " или \r\n — атакующий может внедрить произвольные HTTP-заголовки или сломать ответ. Эндпоинт публичный (без авторизации).
Исправление: санитизировать fname до вставки:
const fname = (req.query.filename || 'white-list.txt').replace(/[^\w\-_.]/g, '_');
🟠 MAJOR
2. ui/api-client.js: таймаут не работает
req.on('timeout', () => { req.destroy(); reject(new Error('API timeout')); });
// ...но req.setTimeout() нигде не вызывается
В Node.js событие timeout на http.ClientRequest срабатывает только если установлен socket timeout через req.setTimeout(ms). Без него запрос может висеть бесконечно при недоступном сервере. Все UI-запросы к API могут зависнуть.
Исправление: добавить req.setTimeout(15000, ...) после создания запроса.
3. Отсутствует шаг 6 в docs/devops-deploy.md
В документе многократно упоминается «шаг 6 — регистрация Keycloak OIDC клиента», но сам шаг не написан. DevOps не сможет развернуть продакшен без понимания как создать Confidential client в Keycloak (Valid Redirect URIs, Client Authentication, получение KC_CLIENT_SECRET).
🟡 MINOR
4. Дублирование X-Forwarded-Proto в nginx config
В docs/devops-deploy.md, в блоке nginx server {} на 443:
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; # ← дубль
Второй заголовок — лишний, не вредит но вводит в заблуждение.
5. JSON.parse без try-catch в ui/api-client.js
const data = ct.includes('application/json') ? JSON.parse(raw) : raw;
Если API вернёт невалидный JSON при content-type: application/json — необработанный SyntaxError упадёт наверх и отдаст 500 без внятного сообщения.
6. Нет ограничения размера ответа IAM API
В fetchIamUser и switchProfile (src/auth.js) ответ накапливается без ограничения:
let data = '';
res.on('data', c => { data += c; });
Злонамеренный или сломанный IAM-сервер может передать гигабайты данных.
Ответы на 5 вопросов
1. Безопасность
| Проверка | Результат |
|---|---|
| Open redirect | ✅ Защищён. safeLocal() и safeReturn() — корректная проверка startsWith('/') && !startsWith('//'). |
| XSS в шаблонах | ✅ EJS использует <%= %> (с экранированием). Helmet + CSP настроены. |
| CSRF | ✅ csrf.js middleware подключён к мутирующим роутам. |
| Утечка токенов | ✅ Токен в сессии (PostgreSQL), не в HTML, не в cookie клиента напрямую. |
| Header injection | ❌ /export — filename из query без санитизации (см. CRITICAL #1). |
| Rate limiting | ✅ authLimiter на POST /login. |
| DEV_MODE в production | ✅ Жёсткая защита — process.exit(1) при NODE_ENV=production && DEV_MODE=true. |
2. IAM интеграция
fetchIamUser и switchProfile — реализованы корректно:
- Timeout: 10 секунд через
req.setTimeout(10000)— OK. - Error handling: IAM недоступен →
console.error+ fallback на JWT claims. Правильно. - Fallback в
/callback(src/routes/oidc.js): вычисляетisAdminпоactiveClientId === ADMIN_CLIENT_ID. Безопасно — токен уже верифицирован подписью. activeProfileIdсохраняется в сессии — переключение компаний через IAM API работает.
Единственный риск: если IAM_API_URL недоступен при логине — isAdmin: false всегда, даже для администратора. Это намеренный безопасный дефолт, но пользователи об этом не уведомляются в UI.
3. Роли/компании
| Аспект | Результат |
|---|---|
isAdmin |
✅ В IAM-режиме — из ui.isAdmin (IAM единственный источник). В fallback — по clientId. |
allClientIds |
✅ resolveCompany() в API проверяет profiles (IAM) или allClientIds (fallback). Изоляция между компаниями соблюдена. |
| Admin ?company= | ✅ parseInt + isFinite + проверка в БД — инъекции через company ID невозможны. |
| UI vs API | ✅ UI полностью делегирует бизнес-логику к API. Расхождений нет. |
| Мульти-компания | ✅ client_id из query проверяется против allowedIds перед использованием. |
4. DevOps
.env.example: достаточен — все обязательные переменные перечислены с пояснениями.
devops-deploy.md: хорошая документация по PostgreSQL, Node.js, nginx, PM2, certbot. Но:
- Шаг 6 (Keycloak OIDC client) — отсутствует (см. MAJOR #3).
- Нет инструкции по ротации SESSION_SECRET при компрометации.
IAM_API_URLзадан по умолчанию в.env.example(не закомментирован) — хорошо, но стоит добавить пометку что это реальный внешний сервис.
5. Код
| Проверка | Результат |
|---|---|
| Незакрытые соединения | ⚠️ ui/api-client.js — timeout event без req.setTimeout() (см. MAJOR #2). fetchIamUser/switchProfile — OK, setTimeout есть. |
| Race conditions | ✅ Отсутствуют. Переключение профиля — sequential await. |
| Циклические require | ✅ ui/routes/entries.js → src/auth (lazy inside handler) — нет цикла. |
| JSON.parse unsafe | ⚠️ ui/api-client.js — без try-catch (см. MINOR #5). |
| Body size limits | ✅ express.urlencoded({ limit: '32kb' }), express.json({ limit: '32kb' }). |
Оценки
| Категория | Оценка | Обоснование |
|---|---|---|
| DevOps | 7 / 10 | Хорошая документация, fail-fast проверки, но нет Keycloak-шага. |
| Код | 7 / 10 | Чистая архитектура API+UI, правильный CSRF/auth. Баг с таймаутом в api-client и header injection снижают. |
Топ-3 что исправить
-
[CRITICAL] Санитизировать
filenameв/export(server.js~87):const fname = (req.query.filename || 'white-list.txt').replace(/[^\w\-_.]/g, '_'); -
[MAJOR] Добавить
req.setTimeout()вui/api-client.js(~35):const req = lib.request(opts, (res) => { ... }); req.setTimeout(15000, () => { req.destroy(); reject(new Error('API timeout')); });Убрать дублирующий
req.on('timeout', ...)или оставить оба. -
[MAJOR] Написать шаг 6 в
docs/devops-deploy.md— регистрация OIDC-клиента в Keycloak:- Тип клиента: Confidential, Authorization Code Flow
- Valid Redirect URIs:
https://ваш-домен/callback - Web Origins:
https://ваш-домен - Как скопировать KC_CLIENT_ID и KC_CLIENT_SECRET