Files
ipwhitelist-app/docs/sonnet-audit.md
T
naeel 58e34042de fix: header injection, api timeout, nginx duplicate + bump 0.5.21
- server.js: санитизация filename в /export (header injection)
- ui/api-client.js: req.setTimeout(15000) + try/catch JSON.parse
- docs/devops-deploy.md: убран дубль X-Forwarded-Proto
2026-06-04 15:41:03 +03:00

9.1 KiB
Raw Blame History

Аудит 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 /exportfilename из 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.jstimeout event без req.setTimeout() (см. MAJOR #2). fetchIamUser/switchProfile — OK, setTimeout есть.
Race conditions Отсутствуют. Переключение профиля — sequential await.
Циклические require ui/routes/entries.jssrc/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 что исправить

  1. [CRITICAL] Санитизировать filename в /export (server.js ~87):

    const fname = (req.query.filename || 'white-list.txt').replace(/[^\w\-_.]/g, '_');
    
  2. [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', ...) или оставить оба.

  3. [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