# Код-ревью 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..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 вручную) ⇒ сторонний сайт может отправить `
` и удалить чужие записи. Если же токен реально приходит только в заголовке 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=` не читается в `/`, сырые ошибки БД наружу.