refactor: csp.js exports CSP_DIRECTIVES, helmet applies them in one call

This commit is contained in:
2026-05-30 15:37:32 +03:00
parent 695264e7b4
commit b6e8f31cc6
2 changed files with 26 additions and 38 deletions
+4 -4
View File
@@ -30,7 +30,7 @@ const { MOCK_USERS, backUrl } = require('./src/config');
const { mutationLimiter, exportLimiter } = require('./src/middleware/rateLimit'); const { mutationLimiter, exportLimiter } = require('./src/middleware/rateLimit');
const { initCsrf } = require('./src/middleware/csrf'); const { initCsrf } = require('./src/middleware/csrf');
const { createSessionMiddleware } = require('./src/middleware/session'); const { createSessionMiddleware } = require('./src/middleware/session');
const { cspMiddleware } = require('./src/middleware/csp'); const { CSP_DIRECTIVES } = require('./src/middleware/csp');
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
@@ -39,9 +39,9 @@ app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views')); app.set('views', path.join(__dirname, 'views'));
// ── Безопасность HTTP-заголовков ─────────────────────────────────────────────── // ── Безопасность HTTP-заголовков ───────────────────────────────────────────────
// helmet выставляет базовые заголовки; CSP вынесен в src/middleware/csp.js. // helmet выставляет все security-заголовки одним вызовом, включая CSP.
app.use(helmet({ contentSecurityPolicy: false })); // Директивы CSP вынесены в src/middleware/csp.js.
app.use(cspMiddleware); app.use(helmet({ contentSecurityPolicy: { directives: CSP_DIRECTIVES } }));
// За reverse-proxy (nginx/k8s ingress) — доверяем первому хопу для secure cookies. // За reverse-proxy (nginx/k8s ingress) — доверяем первому хопу для secure cookies.
if (process.env.NODE_ENV === 'production') app.set('trust proxy', 1); if (process.env.NODE_ENV === 'production') app.set('trust proxy', 1);
+12 -24
View File
@@ -1,51 +1,39 @@
/** /**
* src/middleware/csp.js — Content Security Policy через helmet. * src/middleware/csp.js — Content Security Policy директивы.
* *
* Текущее ограничение: EJS-шаблоны используют inline <style>, inline <script> * Экспортирует объект CSP_DIRECTIVES для передачи в helmet({ contentSecurityPolicy }).
* и атрибуты onclick/style= — поэтому 'unsafe-inline' пока необходим. * Так CSP применяется один раз вместе с остальными заголовками helmet.
*
* Текущее ограничение: EJS-шаблоны используют inline <style>, <script> и
* атрибуты onclick/style= — поэтому 'unsafe-inline' пока необходим.
* Это всё равно лучше отключённого CSP: блокируются внешние ресурсы, * Это всё равно лучше отключённого CSP: блокируются внешние ресурсы,
* фреймирование, object/embed, переопределение base. * фреймирование, object/embed, переопределение base.
* *
* Что блокирует эта политика: * Что блокирует эта политика:
* - Загрузку скриптов/стилей с внешних доменов (CDN-инъекции, supply chain) * - Загрузку скриптов/стилей с внешних доменов
* - Фреймирование страниц (clickjacking) — frame-ancestors 'none' * - Фреймирование страниц (clickjacking) — frame-ancestors 'none'
* - <object>, <embed>, <applet> — object-src 'none' * - <object>, <embed>, <applet> — object-src 'none'
* - Переопределение <base href> — base-uri 'self' * - Переопределение <base href> — base-uri 'self'
* - POST форм на внешние домены — form-action 'self' * - POST форм на внешние домены — form-action 'self'
* *
* TODO (при переносе стилей в .css и скриптов в .js): * TODO (при переносе стилей в .css и скриптов в .js файлы):
* убрать 'unsafe-inline', добавить nonce или hash для оставшихся inline-блоков. * убрать 'unsafe-inline', добавить nonce или hash для оставшихся inline-блоков.
*
* Экспортирует: cspMiddleware — передавать напрямую в app.use().
*/ */
'use strict'; 'use strict';
const helmet = require('helmet'); const CSP_DIRECTIVES = {
/**
* Возвращает helmet CSP middleware.
* Вынесено в функцию чтобы легко заменить директивы без правки server.js.
* @returns {import('express').RequestHandler}
*/
function createCspMiddleware() {
return helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"], defaultSrc: ["'self'"],
// 'unsafe-inline' нужен пока стили и скрипты inline в EJS. // 'unsafe-inline' нужен пока стили и скрипты inline в EJS.
scriptSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"], styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:'], // data: для SVG-иконок если появятся imgSrc: ["'self'", 'data:'], // data: для SVG-иконок
fontSrc: ["'self'"], fontSrc: ["'self'"],
connectSrc: ["'self'"], connectSrc: ["'self'"],
objectSrc: ["'none'"], // блок <object>/<embed>/<applet> objectSrc: ["'none'"], // блок <object>/<embed>/<applet>
baseUri: ["'self'"], // блок переопределения <base> baseUri: ["'self'"], // блок переопределения <base>
formAction: ["'self'"], // POST только на свой домен formAction: ["'self'"], // POST только на свой домен
frameAncestors: ["'none'"], // clickjacking protection frameAncestors: ["'none'"], // clickjacking protection
}, };
});
}
const cspMiddleware = createCspMiddleware(); module.exports = { CSP_DIRECTIVES };
module.exports = { cspMiddleware };