docs: перенос документации из IPWhiteList + CONTEXT.md (резюме для нового чата)
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# Код-ревью server.js — auth, CSRF, XSS, заголовки
|
||||
|
||||
> Дата: 2026-05-30
|
||||
|
||||
## 🔴 Auth bypass 1 — отсутствие проверки подписи JWT (критично)
|
||||
|
||||
```js
|
||||
const payload = JSON.parse(Buffer.from(auth.replace('Bearer ', '').split('.')[1], 'base64').toString());
|
||||
```
|
||||
|
||||
Это не аутентификация — это просто декодирование base64 payload без проверки подписи. Любой может прислать самодельный токен `Bearer xxx.<base64 любого JSON>.yyy` и стать любой компанией. `ClientID`, `company_id` полностью подконтрольны атакующему → полный обход изоляции компаний, доступ к чужим whitelist, экспорт. Это та самая дыра, ради которой нужен JWKS/проверка подписи (см. отдельный prompt-opus-auth). До внедрения проверки подписи продакшен поднимать нельзя.
|
||||
|
||||
Минимум: `jose.jwtVerify(token, JWKS, { issuer: 'auth-api' })` с кэшированием ключей, и брать payload только из верифицированного результата. Также `split('.')[1]` упадёт на токене без точек → попадёт в `catch` → `req.user = {}`.
|
||||
|
||||
## 🔴 Auth bypass 2 — пустой req.user не блокирует доступ
|
||||
|
||||
```js
|
||||
} catch { req.user = {}; }
|
||||
next();
|
||||
```
|
||||
|
||||
При невалидном токене `req.user = {}` и запрос идёт дальше. В `/` тогда `clientId = undefined` → `getOrCreateCompany(undefined, undefined)` создаст/найдёт компанию с `client_id = NULL`. Все анонимы делят одну «нулевую» компанию, видят и редактируют её whitelist. Нет ни одного `return res.status(401)`. Фикс — после catch и для не-DEV пути: если нет `req.user.clientId` → `return res.status(401).send('Unauthorized')`.
|
||||
|
||||
## 🔴 DEV_MODE — риск включения в проде
|
||||
|
||||
```js
|
||||
const DEV = process.env.DEV_MODE === 'true';
|
||||
...
|
||||
if (DEV) { req.user = { ... clientId: 'WZ01325' ... }; return next(); }
|
||||
```
|
||||
|
||||
Если `DEV_MODE=true` случайно попадёт в прод-конфиг — полный обход аутентификации, все становятся `WZ01325`. Нет защиты «DEV только не в production». Добавить страховку: `const DEV = process.env.DEV_MODE === 'true' && process.env.NODE_ENV !== 'production';` и логировать предупреждение при старте, если DEV активен.
|
||||
|
||||
## 🔴 CSRF — все POST-формы без токенов (критично)
|
||||
|
||||
`/add`, `/delete/:id` — обычные form-POST, меняют состояние, без CSRF-токена. В проде аутентификация по cookie/сессии (а не по заголовку Authorization вручную) ⇒ сторонний сайт может отправить `<form action="https://white.../delete/123" method=POST>` и удалить чужие записи. Если же токен реально приходит только в заголовке Authorization (не в cookie), CSRF слабее — но форма в браузере не может сама проставить Authorization, значит модель аутентификации в браузере вообще не работает с текущим кодом. Это надо прояснить (см. questions.md). В любом случае при cookie-сессии нужен CSRF-токен (`csurf` или double-submit) на все POST.
|
||||
|
||||
## 🟡 XSS через ?error= в редиректе
|
||||
|
||||
```js
|
||||
res.redirect('/?error=' + encodeURIComponent(e.message));
|
||||
```
|
||||
|
||||
Сам редирект экранирует. Уязвимость — в шаблоне: если `index.ejs` выводит `error` через `<%- %>` (не экранируя) — рефлексивный XSS. По ревью ejs вывод идёт через `<%= %>`, так что сейчас безопасно. Но `e.message` может содержать сырой текст ошибки БД — нежелательно показывать пользователю (info leak). Маппить на дружелюбные сообщения, не отдавать `e.message` БД наружу.
|
||||
|
||||
Кроме того `/?error=` читается из query, но в роуте `/` параметр `req.query.error` вообще не прокидывается в render (`error: null`) — то есть редирект с `?error=` ничего не покажет. Несоответствие: либо читать `req.query.error`, либо убрать. Если читать — обязательно только экранированный вывод.
|
||||
|
||||
## 🟡 Обработка ошибок — каскад в /add
|
||||
|
||||
```js
|
||||
} catch (e) {
|
||||
const company = await q.getOrCreateCompany(clientId, companyName).catch(() => null);
|
||||
const entries = company ? await q.listEntries(company.id).catch(() => []) : [];
|
||||
...
|
||||
```
|
||||
|
||||
В catch-ветке снова дёргается БД (3 запроса). Если БД легла — все упадут в `.catch(() => ...)` и пользователь увидит исходную ошибку с пустым списком — приемлемо, но шумно. Главное: нет глобального error-handler middleware (`app.use((err, req, res, next) => ...)`) — необработанный промис в любом роуте уронит ответ висящим. Добавить финальный error middleware + `process.on('unhandledRejection')`.
|
||||
|
||||
## 🟡 Нет helmet / security-заголовков
|
||||
|
||||
Отсутствуют `X-Frame-Options`/CSP (clickjacking), `X-Content-Type-Options: nosniff`, `Referrer-Policy`, HSTS. Добавить `helmet()` сразу после создания app. Особенно `frame-ancestors`/`X-Frame-Options: DENY` — UI с формами удаления уязвим к clickjacking.
|
||||
|
||||
## 🟡 Нет rate-limit
|
||||
|
||||
`/add`, `/delete`, `/export` без ограничения частоты. Можно засыпать INSERT-ами/экспортом. Добавить `express-rate-limit` на мутирующие роуты и на `/export`.
|
||||
|
||||
## 🟡 /export — нет авторизации и изоляции (важно по ТЗ)
|
||||
|
||||
```js
|
||||
app.get('/export', async (req, res) => {
|
||||
const cidrs = await q.getExportCIDRs();
|
||||
```
|
||||
|
||||
`getExportCIDRs()` без аргументов — отдаёт CIDR ВСЕХ компаний всем подряд, без проверки `req.user`, без фильтра по компании. Это утечка whitelist всех клиентов. По ТЗ экспорт должен быть либо служебным (ограничен по IP/токену), либо в рамках компании. Сейчас — публичный дамп всех адресов. Требует решения из questions.md (IP-ограничение/служебный токен), но в текущем виде — критичная утечка.
|
||||
|
||||
## 🟡 /healthz выше auth — ок, но раскрывает «OK» только
|
||||
|
||||
`/healthz` объявлен после middleware (значит проходит через auth). При DEV ок; в проде healthz будет требовать токен → проба готовности k8s упадёт. Вынести `/healthz` ВЫШЕ auth middleware.
|
||||
|
||||
## 🟡 Тело запроса без лимита размера
|
||||
|
||||
`express.urlencoded({ extended: true })` без `limit`. DoS большими телами. Поставить `limit: '32kb'`.
|
||||
|
||||
## 🟢 SSRF/path traversal
|
||||
|
||||
`express.static` на фиксированную папку, шаблоны фиксированы — обхода нет.
|
||||
|
||||
---
|
||||
|
||||
**Итог (по приоритету):**
|
||||
1. 🔴 JWT без проверки подписи → полный обход auth и изоляции — блокер прода.
|
||||
2. 🔴 Пустой `req.user = {}` не возвращает 401 → анонимы делят NULL-компанию.
|
||||
3. 🔴 `/export` без авторизации отдаёт CIDR всех компаний — утечка.
|
||||
4. 🔴 DEV_MODE без привязки к NODE_ENV — риск обхода auth в проде.
|
||||
5. 🔴 Нет CSRF на POST-формах.
|
||||
6. 🟡 Нет helmet, rate-limit, лимита тела, глобального error-handler.
|
||||
7. 🟡 `/healthz` под auth (сломает k8s-пробу), `?error=` не читается в `/`, сырые ошибки БД наружу.
|
||||
Reference in New Issue
Block a user