docs: перенос документации из IPWhiteList + CONTEXT.md (резюме для нового чата)
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
# Промпт для Claude Opus 4 — безопасность server.js (auth, CSRF, XSS)
|
||||
|
||||
## Контекст (не анализируй)
|
||||
Node.js + Express + EJS + PostgreSQL. Микросервис IP WhiteList для облачного провайдера. Аутентификация через внешний auth-api (JWT в Authorization header). Сейчас DEV_MODE=true — временная заглушка. Продакшен с реальными токенами.
|
||||
|
||||
## Что нужно
|
||||
Ниже полный код `server.js`. Найди:
|
||||
- Баги в auth middleware (обход при DEV_MODE, пустой req.user)
|
||||
- CSRF-уязвимости (POST-формы без токенов)
|
||||
- XSS через query-параметры (error в редиректе)
|
||||
- Race conditions на уровне роутов
|
||||
- Проблемы обработки ошибок
|
||||
- Небезопасные HTTP-заголовки
|
||||
- Отсутствующие middleware (helmet, rate-limit, etc.)
|
||||
- Несоответствия ТЗ (где обещано, но не реализовано)
|
||||
|
||||
Ограничения:
|
||||
- Не предлагай менять стек
|
||||
- Только конкретные строки и исправления
|
||||
- ВЕСЬ ОТВЕТ ОДНИМ БЛОКОМ — без свёрток, без интерактивных элементов. Чистый текст.
|
||||
|
||||
```js
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
require('dotenv').config();
|
||||
const { checkConnection } = require('./src/db');
|
||||
const q = require('./src/queries');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const DEV = process.env.DEV_MODE === 'true';
|
||||
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// ── Auth middleware ──
|
||||
app.use((req, res, next) => {
|
||||
if (DEV) {
|
||||
req.user = { email: 'dev@test.local', clientId: 'WZ01325', companyId: null, companyName: 'DEV' };
|
||||
return next();
|
||||
}
|
||||
const auth = req.headers.authorization || '';
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(auth.replace('Bearer ', '').split('.')[1], 'base64').toString());
|
||||
req.user = {
|
||||
email: payload.email || 'unknown',
|
||||
clientId: payload.ClientID,
|
||||
companyId: payload.company_id,
|
||||
companyName: payload.company_name || payload.ClientID,
|
||||
};
|
||||
} catch { req.user = {}; }
|
||||
next();
|
||||
});
|
||||
|
||||
// ── Health ──
|
||||
app.get('/healthz', (req, res) => res.send('OK'));
|
||||
|
||||
// ── Главная ──
|
||||
app.get('/', async (req, res) => {
|
||||
const { clientId, companyName } = req.user;
|
||||
try {
|
||||
const company = await q.getOrCreateCompany(clientId, companyName);
|
||||
const limit = await q.getLimit(company);
|
||||
const entries = await q.listEntries(company.id);
|
||||
res.render('index', { entries, limit, used: entries.length, user: req.user, error: null, message: null, wasNormalized: false });
|
||||
} catch (e) {
|
||||
res.render('index', { entries: [], limit: 15, used: 0, user: req.user, error: e.message, message: null, wasNormalized: false });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Создать ──
|
||||
app.post('/add', async (req, res) => {
|
||||
const { value, comment } = req.body;
|
||||
const { clientId, companyName, email } = req.user;
|
||||
try {
|
||||
const company = await q.getOrCreateCompany(clientId, companyName);
|
||||
const result = await q.createEntry(company.id, value, comment, email);
|
||||
const limit = await q.getLimit(company);
|
||||
const entries = await q.listEntries(company.id);
|
||||
res.render('index', {
|
||||
entries, limit, used: entries.length, user: req.user,
|
||||
message: result.wasNormalized ? `Адрес нормализован в ${result.entry.value_cidr}` : 'Добавлено',
|
||||
error: null, wasNormalized: result.wasNormalized,
|
||||
});
|
||||
} catch (e) {
|
||||
const company = await q.getOrCreateCompany(clientId, companyName).catch(() => null);
|
||||
const entries = company ? await q.listEntries(company.id).catch(() => []) : [];
|
||||
const limit = company ? await q.getLimit(company).catch(() => 15) : 15;
|
||||
res.render('index', { entries, limit, used: entries.length, user: req.user, error: e.message, message: null, wasNormalized: false });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Удалить (soft) ──
|
||||
app.post('/delete/:id', async (req, res) => {
|
||||
const { clientId, companyName, email } = req.user;
|
||||
try {
|
||||
const company = await q.getOrCreateCompany(clientId, companyName);
|
||||
await q.deleteEntry(req.params.id, company.id, email);
|
||||
res.redirect('/');
|
||||
} catch (e) {
|
||||
res.redirect('/?error=' + encodeURIComponent(e.message));
|
||||
}
|
||||
});
|
||||
|
||||
// ── Экспорт ──
|
||||
app.get('/export', async (req, res) => {
|
||||
try {
|
||||
const cidrs = await q.getExportCIDRs();
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
res.send(cidrs.join('\n') + '\n');
|
||||
} catch (e) {
|
||||
res.status(500).send('Export error');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Старт ──
|
||||
checkConnection()
|
||||
.then(() => console.log('DB connected'))
|
||||
.catch(e => console.error('DB not ready:', e.message));
|
||||
|
||||
app.listen(PORT, () => console.log(`Server on port ${PORT}`));
|
||||
```
|
||||
Reference in New Issue
Block a user