feat: security audit fixes
This commit is contained in:
@@ -35,17 +35,50 @@ app.use(cookieParser());
|
||||
app.use(traceMiddleware);
|
||||
|
||||
// ── PUBLIC endpoints ─────────────────────────────────────────────────────────
|
||||
app.get('/api/health', (_req, res) => {
|
||||
res.json({ success: true, data: { status: 'ok' } });
|
||||
// H11 — /api/health with DB probe (не возвращает OK если DB down)
|
||||
import { db } from './config/database';
|
||||
app.get('/api/health', async (_req, res) => {
|
||||
try {
|
||||
await Promise.race([
|
||||
db.raw('select 1'),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('db-timeout')), 1000)),
|
||||
]);
|
||||
res.json({ success: true, data: { status: 'ok' } });
|
||||
} catch (err: any) {
|
||||
res.status(503).json({ success: false, error: 'db_unavailable' });
|
||||
}
|
||||
});
|
||||
|
||||
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
||||
app.get('/api/docs/swagger.json', (_req, res) => {
|
||||
// ── Глобальный rate limit на /api/* — ДО docs чтобы не было unauthenticated DoS на swagger.json
|
||||
app.use('/api', globalLimiter);
|
||||
|
||||
// H1 — Swagger gated. В production требуется basic-auth ИЛИ NODE_ENV != production.
|
||||
// JSON endpoint ОБЯЗАТЕЛЬНО до app.use('/api/docs', ...) — иначе swagger-ui-express
|
||||
// перехватывает все /api/docs/* и возвращает HTML вместо JSON.
|
||||
const docsGate = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (process.env.NODE_ENV !== 'production' || process.env.SWAGGER_PUBLIC === 'true') {
|
||||
return next();
|
||||
}
|
||||
// Production без SWAGGER_PUBLIC=true → require basic auth (operator credentials)
|
||||
const auth = req.headers.authorization || '';
|
||||
const expected = process.env.SWAGGER_BASIC_AUTH; // "user:pass"
|
||||
if (!expected || !auth.startsWith('Basic ')) {
|
||||
res.set('WWW-Authenticate', 'Basic realm="docs"');
|
||||
res.status(401).json({ success: false, error: 'Docs auth required' });
|
||||
return;
|
||||
}
|
||||
const decoded = Buffer.from(auth.slice(6), 'base64').toString('utf8');
|
||||
if (decoded !== expected) {
|
||||
res.set('WWW-Authenticate', 'Basic realm="docs"');
|
||||
res.status(401).json({ success: false, error: 'Invalid docs credentials' });
|
||||
return;
|
||||
}
|
||||
return next();
|
||||
};
|
||||
app.get('/api/docs/swagger.json', docsGate, (_req, res) => {
|
||||
res.json(swaggerSpec);
|
||||
});
|
||||
|
||||
// ── Глобальный rate limit на весь API после public endpoints ────────────────
|
||||
app.use('/api', globalLimiter);
|
||||
app.use('/api/docs', docsGate, swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
||||
|
||||
// ── PROTECTED endpoints (JWT + CSRF) ─────────────────────────────────────────
|
||||
const protect = [authMiddleware, csrfMiddleware];
|
||||
|
||||
Reference in New Issue
Block a user