Files
ipwhitelist-app/docs/deepseek-audit.md
T
naeel c1df27d29d fix: DeepSeek audit — header injection + IAM limit + bump 0.5.22
- src/api/routes/entries.js: санитизация filename в /api/v1/entries/export
- ui/routes/export.js: санитизация filename в UI /export
- src/auth.js: лимит 100KB для IAM-ответов
- src/api/routes/entries.js: fallback для пустого allowedIds
2026-06-04 15:48:16 +03:00

14 KiB
Raw Blame History

Аудит ipwhitelist-app — DeepSeek V4 Pro

Дата: 2026-06-04
Файлы проверены: все 30+ файлов из src/, ui/, views/, docs/, server.js, package.json, .env.example, sql/schema.sql


Вердикт

С оговорками — код высокого качества, архитектура продумана, но есть 2 проблемы которые нужно исправить перед production: CSRF не подключён и две /export-ручки без санитизации filename.


1. Найденные проблемы

🔴 CRITICAL

C1. Header injection в двух /export-ручках (не санитизирован filename)

В проекте три эндпоинта /export. Санитизация filename добавлена только в одном (server.js:93 — публичный /export). Два других — без защиты:

Файл Строка Статус
server.js:93 (публичный /export) replace(/[^\w\-_. ]/g, '_') исправлено
src/api/routes/entries.js:104 (/api/v1/entries/export) req.query.filename || 'white-list.txt' без санитизации
ui/routes/export.js:32 (UI /export за авторизацией) req.query.filename || 'white-list.txt' без санитизации

Атакующий с Bearer-токеном может внедрить \r\n в Content-Disposition через ?filename=...%0d%0a....

Рекомендация: вынести санитизацию в хелпер src/config.js и использовать во всех трёх местах.


🟠 MAJOR

M1. CSRF-защита реализована, но НЕ подключена

src/middleware/csrf.js содержит initCsrf() с doubleCsrf (csrf-csrf v4) — современная замена deprecated csurf. Middleware правильно настроен: cookie csrf-token, getCsrfTokenFromRequest: (req) => req.body._csrf, HMAC-подпись.

Но initCsrf() нигде не вызывается. Ни в server.js, ни в ui/index.js, ни в роутах.

Во всех EJS-шаблонах csrfToken передаётся как пустая строка:

  • views/index.ejscsrfToken: ''
  • views/admin.ejscsrfToken: ''

Формы содержат <input type="hidden" name="_csrf" value="<%= csrfToken %>"> — токен всегда пустой, проверка никогда не срабатывает.

Рекомендация: вызвать initCsrf() в server.js → start(), передать doubleCsrfProtection в UI-роуты, generateCsrfToken — в GET-обработчики. API-слой (/api/v1/*) не требует CSRF (Bearer-токены).


M2. IAM API — нет ограничения размера ответа

fetchIamUser и switchProfile в src/auth.js накапливают ответ без лимита:

let data = '';
res.on('data', c => { data += c; });

Скомпрометированный или сломанный IAM-сервер может исчерпать память процесса.

Рекомендация: добавить проверку if (data.length > 100_000) { req.destroy(); reject(...) }.


M3. resolveCompany в API entries.js — хрупкий profiles/allClientIds fallback

Строки 4758:

const allowedIds = req.user.profiles && req.user.profiles.length
  ? req.user.profiles.map(p => p.client_id)
  : (req.user.allClientIds || []);

Когда IAM недоступен, req.user.profilesundefined. Fallback на allClientIds корректен, НО: если пользователь переключил компанию через UI (activeClientId обновлён в сессии), а API-слой этого не видит (API читает req.user из Bearer-токена, не из сессии), то client_id из query может не пройти проверку allowedIds.includes(requestedId) — и переключение молча проигнорируется.

Рекомендация: в API-слое добавить fallback: если allowedIds пуст — разрешить любой client_id из query (доверять тому, что в токене).


🟡 MINOR

m1. Отсутствует proxy_read_timeout в nginx-конфиге docs/devops-deploy.md

Nginx по умолчанию обрывает соединение через 60с. Для /export (агрегация тысяч CIDR) этого может не хватить. Добавить proxy_read_timeout 120s;.

m2. server.jscheckConnection() вызывается без await

Строка 127:

checkConnection()
  .then(() => console.log('DB connected'))
  .catch(e => console.error('DB not ready:', e.message));

Приложение стартует до проверки соединения с БД. Если БД недоступна — сервер отвечает 500 на все запросы, но не падает. Лучше: await checkConnection() внутри start() до app.listen().

m3. ui/routes/auth.js —不一致的 safeReturn

POST /login-token:

  • Ветка tokenres.redirect(safeReturn(returnTo) || '/')
  • Ветка clientIdres.redirect(returnTo || '/') без safeReturn

Параметр returnTo идёт из скрытого поля формы, но теоретически может быть подменён. Низкий риск, но inconsistent.

m4. src/queries.js:getExportCIDRs — нет LIMIT

При вызове без companyId (admin) запрос возвращает ВСЕ CIDR всех компаний. Для агрегации это ожидаемо, но при 100k+ записей — memory spike.

m5. src/queries.js:getAudit — hardcoded LIMIT 500 без пагинации

Для production с сотнями компаний 500 строк аудита — мало. Нужна пагинация.

m6. ui/routes/entries.js:42require('../../src/auth') внутри обработчика

Динамический require в рантайме (для switchProfile). Node.js кеширует, так что performance impact минимален, но это «code smell». Лучше прокинуть auth через фабрику createRouter({ auth }).


2. Безопасность — полный разбор

Вектор Статус Комментарий
Open redirect /login?returnTo= safeReturn() + safeLocal()startsWith('/') && !startsWith('//')
Open redirect /callback safeLocal() на oidcReturnTo
XSS (EJS) <%= %> везде (экранирование), helmet + CSP
CSRF Код написан, но не подключён (см. M1)
Header injection ⚠️ Частично исправлено (см. C1)
Clickjacking frameAncestors: ["'none'"] в CSP
Rate limiting authLimiter (10/5min), mutationLimiter (30/min), exportLimiter (20/min)
DEV_MODE в production Жёсткий process.exit(1)
Session fixation express-session + connect-pg-simple, sessionID меняется при логине
SQL injection Parameterized queries ($1, $2) везде
Secret management Все секреты из process.env, fail-fast для SESSION_SECRET и IAM_API_URL
JWT validation RS256 + JWKS (Keycloak), issuer check, audience check

3. IAM-интеграция — полный разбор

Аспект Статус Комментарий
fetchIamUser timeout 10 секунд через req.setTimeout(10000)
fetchIamUser error handling try/catch с понятным сообщением
switchProfile Корректный POST с profile_id в body
OIDC callback IAM fallback При недоступности IAM — fallback на JWT claims с логом
login-token IAM enrichment Вызывается только в OIDC-режиме (auth.isOidc)
userFromPayload с iamData profiles, allClientIds, isAdmin — всё из IAM
resolveCompany multi-profile ⚠️ Хрупкий fallback (см. M3)
IAM response size limit Нет ограничения (см. M2)
Разные стенды IAM IAM_API_URL в .env, dev/test/prod через переменную

4. Корректность кода — полный разбор

Аспект Статус Комментарий
Циклические require Чистая архитектура: config.jsauth.jsqueries.jsdb.js. Нет циклов
DB connection pool pg.Pool, max=10, idleTimeout=30s, error listener
Транзакции BEGIN/FOR UPDATE/COMMIT/ROLLBACK в createEntry, updateEntry, deleteEntry
Mock-режим Локальная RSA-пара, /dev-login, DEV_MODE/DEV_SECRET
Middleware order helmet → static → session → OIDC → API → UI → error handler
Promise error handling Все async-обработчики обёрнуты в try/catch
checkConnection ⚠️ Не await (см. m2)
rangeToCIDRs Элегантный bit-twiddling алгоритм
aggregateCIDRs Scan-line merge + rangeToCIDRs
Уникальность CIDR Partial unique index WHERE deleted_at IS NULL
Soft delete deleted_at + deleted_by, записи не удаляются физически

5. DevOps — полный разбор

Аспект Статус Комментарий
Пошаговая инструкция 6 шагов: PostgreSQL → Node.js → .env → nginx → PM2 → Keycloak
Все зависимости Node.js 20, PostgreSQL 16, nginx, certbot, PM2
Переменные окружения .env.example с комментариями, команды генерации секретов
Схема БД sql/schema.sql с индексами и constraints
HTTPS nginx + certbot, автообновление
Keycloak OIDC Пошаговая инструкция регистрации клиента (шаг 6)
PM2 автозапуск pm2 startup systemd + pm2 save
Противоречия в документах Нет — все ссылки согласованы
proxy_read_timeout ⚠️ Отсутствует (см. m1)

6. Оценки

Категория Оценка Обоснование
DevOps-готовность 9/10 Понятная инструкция, все зависимости описаны, OIDC-клиент документирован. Не хватает proxy_read_timeout в nginx.
Код: безопасность 7/10 JWT, CSP, helmet, rate-limit — на высоте. Но CSRF не подключён и две /export-ручки без санитизации filename.
Код: архитектура 9/10 Чистое разделение API/UI, middleware chain прозрачен, нет циклических зависимостей.
Код: IAM интеграция 8/10 Fallback при недоступности IAM, timeout, switchProfile. Нет лимита на ответ IAM.
Тестирование 9/10 121 API-тест, 68 UI-тестов, 47+111 compliance-тестов, 191 стресс-тест.
Общая оценка 8/10 Крепкий продакшен-реди код. 2 обязательных фикса перед деплоем.

7. Топ-5 что исправить перед production

  1. Подключить CSRF — вызвать initCsrf() в server.js, передать в UI-роуты, заполнить csrfToken в шаблонах (M1)
  2. Санитизировать filename в двух оставшихся /export-ручкахsrc/api/routes/entries.js:104 и ui/routes/export.js:32 (C1)
  3. Добавить Content-Length проверку в fetchIamUser/switchProfile — лимит 100KB (M2)
  4. Добавить proxy_read_timeout 120s в nginx-конфиг в docs/devops-deploy.md (m1)
  5. Унифицировать safeReturn в ui/routes/auth.js — добавить в ветку clientId (m3)

8. Сравнение с аудитом Claude Sonnet 4.6

Sonnet проверил 11 файлов и нашёл 6 проблем (1 critical, 2 major, 3 minor). Все его находки подтверждаю.

Что Sonnet пропустил (и я нашёл дополнительно):

Проблема Критичность
CSRF middleware не подключён (код есть, но не wired) 🔴 MAJOR
Две /export-ручки без санитизации filename (проверил только публичную) 🔴 CRITICAL
IAM API без ограничения размера ответа 🟠 MAJOR
resolveCompany — хрупкий profiles/allClientIds fallback 🟠 MAJOR
checkConnection() без await 🟡 MINOR
Не-conсистентный safeReturn в ui/routes/auth.js 🟡 MINOR
getExportCIDRs без LIMIT 🟡 MINOR
getAudit без пагинации 🟡 MINOR
Динамический require в ui/routes/entries.js 🟡 MINOR

Итого: Sonnet нашёл 6 проблем, я подтверждаю все + дополнительно 9. Основное упущение Sonnet — он не проверил, что initCsrf() нигде не вызывается, и не заметил что санитизация filename применена только к одной из трёх export-ручек.