# Код-ревью 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=` не читается в `/`, сырые ошибки БД наружу.
