Информационная безопасность с HelmetJS
Для написания статьи использованы источники: helmetjs и freecodecamp.
Установка и подключение Helmet
Helmet поможет вам защитить ваши Express приложения, установив различные HTTP заголовки.
Установите пакет Helmet:
$ npm install helmet
Затем используйте его в своем коде:
var helmet = require('helmet');
app.use(helmet());
helmet.hidePoweredBy()
скрывает потенциально опасную информацию
Хакеры могут использовать известные уязвимости в Express/Node, если они видят, что ваш сайт работает на Express. На каждый клиентский запрос Express отправляет ответ, в котором, по умолчанию, присутствует HTTP заголовок X-Powered-By: Express
. Промежуточный обработчик helmet.hidePoweredBy()
удаляет заголовок X-Powered-By.
var helmet = require('helmet');
app.use(helmet.hidePoweredBy());
Вы также можете явно установить заголовок на что-то другое, чтобы сбить людей с толку, например,
var helmet = require('helmet');
app.use(helmet.hidePoweredBy({ setTo: 'PHP 4.2.0' }));
helmet.frameguard()
уменьшает риск взлома
Ваша страница может быть помещена в <frame>
или <iframe>
без вашего согласия. Это может привести, в частности, к кликджекинг атакам. Clickjacking - это метод обмана пользователя, заставляющий его взаимодействовать со страницей, отличной от той, которую он видит. Этого можно добиться, запустив вашу страницу с вредоносным содержимым, с помощью iframing. В этом содержимом хакер может наложить на вашу страницу скрытый слой. Скрытые кнопки могут быть использованы для запуска плохих сценариев.
Чтобы браузер не помещал вашу веб-страницу в iframe нужно послать ему заголовок X-Frame-Options. Когда браузеры загружают iframes, они проверяют значение заголовка X-Frame-Options и прерывают загрузку, если это не разрешено.
Заголовок имеет три варианта:
X-Frame-Options: DENY
не позволит никому помещать эту страницу в iframe.
X-Frame-Options: SAMEORIGIN
только вы можете поместить свои собственные страницы в iframes, и никто другой .
X-Frame-Options: ALLOW-FROM http://example.com
позволит только сайту http://example.com поместить вашу страницу в iframe, и никому больше. (Параметр ALLOW-FROM заголовка не поддерживается во многих браузерах. Эти браузеры будут игнорировать весь заголовок, и фрейм будет отображаться, поэтому вам следует избегать использования этой опции.)
В Helmet есть промежуточный обработчик helmet.frameguard()
, который устанавливает заголовок X-Frame-Options.
Обработчик можно использовать как часть Helmet:
var helmet = require('helmet');
app.use(helmet.frameguard({ action: 'sameorigin' }));
Можно, также, использовать его в качестве автономного модуля:
//Убедитесь, что вы запустили "npm install frameguard", чтобы получить пакет Frameguard.
const frameguard = require('frameguard');
app.use(frameguard({ action: 'deny' }));
helmet.xssFilter()
уменьшает риск межсайтовых скриптовых атак (XSS)
Межсайтовые скриптинг (XSS) - это частый тип атак, когда вредоносные скрипты вводятся на уязвимые страницы с целью кражи конфиденциальных данных, таких как сеансовые cookie или пароли. Цель атаки XSS - получить контроль над JavaScript в браузере жертвы. После того, как хакер сделал это, есть много неприятных вещей, которые он может сделать: регистрировать ваши действия, выдавать себя за вас, красть ваши аутентификационные файлы cookie и многое другое.
Один из видов XSS называется "Reflected XSS". Как правило, он работает, устанавливая строку запроса, которая помещается непосредственно в HTML. Включение JavaScript в строку запроса может позволить злоумышленнику выполнить свой JavaScript, просто предоставив кому-то вредоносную строку запроса.
Основное правило для снижения риска XSS атак очень простое: "никогда не доверяйте вводу пользователя". Как разработчик вы всегда должны очищать все входные данные, поступающие извне. Это включает в себя данные, поступающие из форм, URL-адреса запросов GET и даже из тел сообщений. Очистка означает, что вы должны найти и закодировать символы, которые могут быть опасны, например <,>.
Современные браузеры могут помочь снизить риск, приняв более эффективные стратегии программного обеспечения. Часто они настраиваются с помощью HTTP-заголовков.
HTTP-заголовок X-XSS-Protection является базовой защитой. Браузер обнаруживает потенциальный введенный сценарий с помощью эвристического фильтра. Если заголовок включен, браузер изменяет код скрипта, нейтрализуя его.
// Убедитесь, что вы установили пакет Helmet с помощью "npm install helmet".
const helmet = require('helmet');
// Устанавливает "X-XSS-Protection: 1; mode=block".
app.use(helmet.xssFilter());
Более подробно о helmet.xssFilter() читайте здесь.
helmet.noSniff()
не позволяет клиенту угатывать в ответе MIME-тип
Когда поступает ответ от сервера, браузеры могут использовать MIME-сниффинг для адаптации к различным типам данных. MIME-сниффинг - это способ определения MIME-типов у файлов, которые поступают к клиенту от сервера. Изображения PNG имеют MIME-тип image/png
; файлы JSON - application/json
; файлы JavaScript имеют, обычно, MIME-тип text/javascript
.
MIME-сниффинг может быть вектором атаки. Пользователь может загрузить файл изображения с расширением .jpg
, но его содержимое на самом деле будет HTML. И когда вы захотите просмотреть это изображение, браузер "запустит" HTML-страницу, которая может содержать вредоносный JavaScript код. Пожалуй, самая отвратительная атака называется Rosetta Flash, которая позволяет кому-то загружать вредоносный Flash-плагин вместо данных!
Чтобы предотвратить сниффинг MIME-типов, нужно установить заголовок X-Content-Type-Options
в nosniff
. Это заставит браузеры доверять тому, что говорит сервер, и блокировать ресурс, если он ошибается.
Helmet имеет промежуточный обработчик noSniff, который устанавливает заголовок X-Content-Type-Options в nosniff для каждого запроса, и предотвращает попытки браузеров угадать MIME-тип.
// Убедитесь, что вы установили пакет Helmet с помощью "npm install helmet".
const helmet = require('helmet');
// Устанавливает "X-Content-Type-Options: nosniff".
app.use(helmet.noSniff());
Более подробно о helmet.noSniff()
читайте здесь.
helmet.ieNoOpen()
не допускает, чтобы IE открывал ненадежный HTML
Эта атака затрагивает только старые версии Internet Explorer.
Некоторые веб-приложения могут запускать ненадежный HTML при загрузке. Например, вы можете разрешить пользователям загружать и скачивать HTML-файлы. По умолчанию старые версии Internet Explorer позволяют открывать эти HTML-файлы в контексте вашего сайта, что означает, что ненадежная HTML-страница может начать делать плохие вещи в контексте ваших страниц. Дополнительные сведения см. в этом посте MSDN.
В Helmet есть промежуточный обработчик ieNoOpen, который устанавливает заголовок X-Download-Options
в значение no open
для предотвращения открытия скачанных файлов в контексте вашего сайта.
// Убедитесь, что вы установили пакет Helmet с помощью "npm install helmet".
const helmet = require('helmet');
// Устанавливает "X-Download-Options: noopen".
app.use(helmet.ieNoOpen());
Более подробно о helmet.ieNoOpen()
читайте здесь.
helmet.hsts()
просит браузеры получать доступ к вашему сайту только через HTTPS
HTTP Strict Transport Security (HSTS) - это политика веб-безопасности, которая помогает защитить веб-сайты от атак низкоуровневого протокола и перехвата файлов cookie. Если ваш сайт доступен по протоколу HTTPS, вы можете попросить браузеры пользователей избегать использования небезопасного HTTP. Установив заголовок Strict-Transport-Security
, вы говорите браузерам использовать HTTPS для будущих запросов в течение заданного промежутка времени. Это будет работать для запросов, поступающих после первоначального запроса.
Заголовок HTTP Strict-Transport-Security
говорит браузерам придерживаться HTTPS и никогда не посещать небезопасную версию HTTP. Как только браузер увидит этот заголовок, он будет посещать сайт только по протоколу HTTPS в течение следующих 60 дней:
Strict-Transport-Security: max-age=5184000
С помощью Helmet установить заголовок можно так
// Убедитесь, что вы установили пакет Helmet с помощью "npm install helmet".
const helmet = require('helmet');
// Устанавливает "Strict-Transport-Security: max-age=5184000";
// директива includeSubDomains включена по умолчанию.
const sixtyDaysInSeconds = 5184000;
app.use(
helmet.hsts({
maxAge: sixtyDaysInSeconds,
})
);
Обратите внимание, что заголовок не будет указывать пользователям, использующим HTTP переключаться на HTTPS, он просто скажет пользователям HTTPS оставаться на этом протоколе. Вы можете применить HTTPS с помощью модуля express-enforces-ssl.
Директива includeSubDomains включена по умолчанию. Чтобы не распространять это правило на поддомены, установите для параметра includeSubDomains значение false.
const sixtyDaysInSeconds = 5184000;
app.use(
helmet.hsts({
maxAge: sixtyDaysInSeconds,
includeSubDomains: false,
})
);
Примечание: настройка HTTPS на веб-сайте требует приобретения домена и сертификата SSL/TSL.
Более подробно о helmet.hsts()
читайте здесь.
helmet.dnsPrefetchControl()
отключает предварительную выборку DNS
Для повышения производительности большинство браузеров предварительно выбирают DNS-записи для ссылок на странице. Таким образом, конечный ip-адрес уже известен, когда пользователь нажимает на ссылку. Это может привести к чрезмерному использованию службы DNS (если у вас есть большой сайт, посещаемый миллионами людей...), проблемам с конфиденциальностью (один подслушивающий может сделать вывод, что вы находитесь на определенной странице) или изменению статистики страниц (некоторые ссылки могут появиться посещенными, даже если они не являются таковыми). Если у вас есть высокие требования к безопасности, вы можете отключить предварительную выборку DNS, за счет снижения производительности.
Заголовок X-DNS-Prefetch-Control сообщает браузерам, следует ли им выполнять предварительную выборку DNS. Включение его может не сработать — не все браузеры поддерживают его во всех ситуациях — но выключение должно отключить его во всех поддерживаемых браузерах.
Чтобы отключить предварительную выборку DNS, установите заголовок X-DNS-Prefetch-Control
в положение off
. Чтобы попытаться включить его, установите значение on
.
Большинство браузеров не выполняют предварительную выборку DNS, поэтому большинство браузеров могут игнорировать этот заголовок.
// Убедитесь, что вы установили пакет Helmet с помощью "npm install helmet".
const helmet = require('helmet');
// Устанавливает "X-DNS-Prefetch-Control: off".
app.use(helmet.dnsPrefetchControl());
Более подробно о helmet.dnsPrefetchControl()
читайте здесь.
helmet.noCache()
отключает кэширование на стороне клиента
Это промежуточный обработчик устарел. Он будет удален в Helmet 4. Вы все еще можете прочитать документацию здесь.
Если вы выпускаете обновление для своего веб-сайта и хотите, чтобы пользователи всегда загружали более новую версию, вы можете (попытаться) отключить кэширование в браузере клиента. Это может быть полезно и при разработке. Кэширование увеличивает производительность, поэтому отключайте кэширование только тогда, когда есть реальная необходимость.
Этот модуль не защищает от конкретной атаки. Он не позволяет пользователям получать кэшированные версии ваших файлов (например, старый JavaScript).
Представьте, что у вас есть фронтенд веб-приложение, которое работает на JavaScript. И вы обнаружили, что одна из ваших JavaScript библиотек имеет уязвимость, естесственно, вы обновите ее и обновите свой сайт. Но пользователи могут получить старые, кэшированные версии вашего кода, которые все еще имеют эту уязвимость.
Кэширование имеет много преимуществ, но это может привести к тому, что пользователи получат устаревшие версии.
Модуль helmet.noCache()
устанавливает четыре HTTP-заголовка кэширования: Cache-Control
, Surrogate-Control
, Pragma
и Expires
.
// Убедитесь, что вы установили пакет Helmet с помощью "npm install helmet".
const noCache = require('nocache');
app.use(helmet.noCache());
Более подробно о helmet.noCache()
читайте здесь.
helmet.contentSecurityPolicy() устанавливает политику безопасности контента
Хакеры могут сделать много плохого, если они смогут разместить на ваши веб-страницы JavaScript, CSS, плагины и многое другое.
Одна из хитростей этих инъекционных атак заключается в том, что браузер не знает, что хорошо, а что плохо. Как он может определить разницу между легальным файлом JavaScript и вредоносным? Во многих случаях это невозможно..., если только вы не определили политику безопасности контента (Content Security Policy, или CSP).
Самая отвратительная атака - это, вероятно, межсайтовый скриптинг (XSS), когда хакер помещает вредоносный JavaScript код на вашу страницу. Если хакеру удастся запустить JavaScript на вашей странице, то он сможет сделать много плохого, от кражи аутентификационных cookies до регистрирования каждого действия пользователя.
Но даже если злоумышленники не смогут выполнить JavaScript, они могут сделать, много чего другого. Например, если возможно поместить крошечное, прозрачное изображение размером 1x1 на ваш сайт, то можно получить довольно хорошее представление о том, сколько трафика получает ваш сайт. Если возможно запустить уязвимый плагин для браузера, такой как Flash, то можно использовать его недостатки и делать много нехороших вещей.
Большинство современных браузеров поддерживают HTTP заголовок под названием Content-Security-Policy
, который фактически является белым списком вещей, которые могут быть на вашей странице. Вы можете внести в белый список JavaScript, CSS, изображения, плагины и многое другое. Вещи включаются выборочно, поэтому вы говорите, "этот материал разрешен" вместо "этот материал не разрешен".
Допустим, у вас есть веб-сайт, который вообще не ссылается на внешние ресурсы — только ваши материалы. Вы можете установить заголовок, который выглядит следующим образом:
Content-Security-Policy: default-src 'self'
Это фактически говорит браузеру "загружайте только те вещи, которые находятся в моем домене”. Если ттвой сайт - example.com и пользователь пытается загрузить https://example.com/my-javascript.js, то это сработает. Но если пользователь попытается загрузить http://evil.com/evil.js - оно вообще не будет загружаться!
Теперь предположим, что вы хотите также разрешить CSS из CDN Bootstrap. Вы можете установить CSP, который выглядит следующим образом:
Content-Security-Policy: default-src 'self'; style-src 'self' maxcdn.bootstrapcdn.com
Теперь у нас есть белый список 'self' и maxcdn.bootstrapcdn.com. Пользователь сможет загрузить оттуда CSS, но ничего больше. Пользователь даже не сможет загрузить JavaScript или изображения с этого URL-адреса - только таблицы стилей.
CSP работает, определяя белый список источников контента, которым доверяют. Вы можете настроить их для каждого вида ресурсов, которые могут понадобиться веб-странице (скрипты, таблицы стилей, шрифты, фреймы, медиа и т. д.). Существует несколько доступных директив, поэтому владелец веб-сайта может иметь детализированный контроль. К сожалению, CSP не поддерживается старыми браузерами.
В CSP есть много нюансов: то, что вы можете и не можете внести в белый список, поддержка браузером различных функций и альтернативные заголовки. См. материалы ниже для получения дополнительной информации.
- An introduction to Content Security Policy on HTML5 Rocks
- Content Security Policy Reference
- Can I Use Content Security Policy 1.0
- Can I Use Content Security Policy 2.0
Модуль Helmet csp помогает устанавливать политику безопасности контента.
// Убедитесь, что вы установили пакет Helmet с помощью "npm install helmet".
const helmet = require('helmet');
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", 'maxcdn.bootstrapcdn.com'],
},
})
);
Все ваши директивы CSP (например, default-src, style-src) помещаются под параметром directives.
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'", 'default.com'],
scriptSrc: ["'self'", "'unsafe-inline'"],
sandbox: ['allow-forms', 'allow-scripts'],
reportUri: '/report-violation',
objectSrc: ["'none'"],
upgradeInsecureRequests: true,
workerSrc: false, // Это не установлено.
},
})
);
Директивы можно записывать в виде kebab-cased (например, script-src) или camel-cased (например, scriptSrc); эти записи эквивалентны.
Поддерживаются следующие директивы:
- base-uri or baseUri
- block-all-mixed-content or blockAllMixedContent
- child-src or childSrc
- connect-src or connectSrc
- default-src or defaultSrc
- font-src or fontSrc
- form-action or formAction
- frame-ancestors or frameAncestors
- frame-src or frameSrc -img-src or imgSrc
- manifest-src or manifestSrc
- media-src or mediaSrc
- object-src or objectSrc
- plugin-types or pluginTypes
- prefetch-src or prefetchSrc
- report-to or reportTo
- report-uri or reportUri
- require-sri-for or requireSriFor
- sandbox or sandbox
- script-src or scriptSrc
- style-src or styleSrc
- upgrade-insecure-requests or upgradeInsecureRequests
- worker-src or workerSrc
Нарушения CSP
Если вы указали reportUri
, браузеры будут публиковать любые нарушения CSP на вашем сервере. Вот простой пример маршрута Express, который обрабатывает эти отчеты:
Нет ни одной конкретной атаки, которую модуль CSP предотвращает. Главное вот что: вы не хотите, чтобы кто-то поместил на ваши веб-страницы то, чего вы не ожидаете.
По умолчанию директивы широко открыты, поэтому важно установить директиву defaultSrc в качестве запасного варианта. Helmet поддерживает как defaultSrc, так и, по умолчанию, стили, называемые src. Резервный вариант применяется для большинства неопределенных директив.
// Сначала вам нужен парсер JSON.
app.use(
bodyParser.json({
type: ['json', 'application/csp-report'],
})
);
app.post('/report-violation', (req, res) => {
if (req.body) {
console.log('CSP Violation: ', req.body);
} else {
console.log('CSP Violation: No data received!');
}
res.status(204).end();
});
Не все браузеры отсылают нарушения CSP одинаково, поэтому это может потребовать небольшой работы.
Примечание: Если вы используете модуль CSRF, например, такой как csurf, у вас могут возникнуть проблемы с обработкой этих нарушений без валидного CSRF токена. Решить проблему можно, если расположить маршрут отчета CSP выше промежуточного обработчика csurf.
Опция reportOnly
этого модуля будет переключать заголовок Content-Security-Policy-Report-Only
. Это дает браузеру указание сообщать о нарушениях в reportUri
(если указано), но он не блокирует загрузку каких-либо ресурсов.
app.use(helmet.contentSecurityPolicy({
directives: {
// ...
},
reportOnly: true
})
Вы также можете установить это в функцию, чтобы динамически решать, следует ли использовать режим reportOnly
. Вы могли бы использовать это для динамического отключения. Эта функция будет вызываться вместе с объектами запроса и ответа и должна возвращать логическое значение.
app.use(helmet.contentSecurityPolicy({
directives: {
// ...
},
reportOnly: (req, res) => req.query.cspmode === 'debug'
})
Браузерный сниффинг
По умолчанию этот модуль будет просматривать входящий заголовок User-Agent
и отправлять различные заголовки в зависимости от обнаруженного браузера. Например, Chrome до версии 25 использует альтернативный заголовок под названием X-WebKit-CSP
, и этот модуль обрабатывает его. Если браузер не обнаружен, этот модуль установит все заголовки со спецификацией 2.0.
Чтобы отключить браузерный сниффинг и считать его современным браузером, установите для параметра browserSniff
значение false
.
app.use(helmet.contentSecurityPolicy({
directives: {
// ...
},
browserSniff: false
})
Чтобы установить все заголовки, включая устаревшие, установите для параметра setAllHeaders
значение true
. Обратите внимание, что это изменит значение заголовков на основе User-Agent
. Вы можете отключить это, используя опцию browserSniff: false
выше.
app.use(helmet.contentSecurityPolicy({
directives: {
// ...
},
setAllHeaders: true
})
Старые браузеры Android могут быть очень глючными. По умолчанию, это значение равно false.
app.use(helmet.contentSecurityPolicy({
directives: {
// ...
},
disableAndroid: true
})
Генерация nonces
Вы можете динамически генерировать nonces
, чтобы позволить встроенным тегам <script> безопасно анализироваться. Вот простой пример:
const uuidv4 = require('uuid/v4');
app.use(function (req, res, next) {
res.locals.nonce = uuidv4();
next();
});
app.use(
helmet.contentSecurityPolicy({
directives: {
scriptSrc: [
"'self'",
(req, res) => `'nonce-${res.locals.nonce}'`, // 'nonce-614d9122-d5b0-4760-aecf-3a5d17cf0ac9'
],
},
})
);
app.use(function (req, res) {
res.end(`<script nonce="${res.locals.nonce}">alert(1 + 1);</script>`);
});
Использование CSP с CDN
Поведение CSP по умолчанию - это создание заголовков, адаптированных для браузера, который запрашивает вашу страницу. Если у вас есть CDN перед вашим приложением, CDN может кэшировать неправильные заголовки, что делает ваш CSP бесполезным. Убедитесь, что CDN не используется, когда используется этот модуль, или установите параметр browserSniff
в значение false
.
Более подробно о helmet.contentSecurityPolicy()
читайте здесь.
Настройка Helmet используя 'родительский' промежуточный обработчик helmet()
app.use(helmet()) автоматически включает все промежуточные обработчики, представленное выше, за исключением noCache() и contentSecurityPolicy(), но они могут быть включены при необходимости. Вы также можете отключить или настроить любые другие промежуточные обработчики индивидуально, используя объект конфигурации.
Пример:
app.use(
helmet({
frameguard: {
// конфигурирование
action: 'deny',
},
contentSecurityPolicy: {
// включение и конфигурирование
directives: {
defaultSrc: ['self'],
styleSrc: ['style.com'],
},
},
dnsPrefetchControl: false, // отключение
})
);
В этом руководстве каждый промежуточный обработчик показан отдельно для целей обучения. Использование 'родительского' промежуточного обработчика helmet()
легко реализовать в реальном проекте.