refactor: csp.js exports CSP_DIRECTIVES, helmet applies them in one call
This commit is contained in:
@@ -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
@@ -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 };
|
|
||||||
|
|||||||
Reference in New Issue
Block a user