Статья по безопасности NodeJS
Введение
В этой статье перечислены действия, которые разработчики могут предпринять для разработки безопасных приложений Node.js. Каждый элемент имеет краткое объяснение и решение, специфичное для среды Node.js.
Контекст
Число приложений Node.js увеличивается, и они ничем не отличаются от других фреймворков и языков программирования. Приложения Node.js подвержены всевозможным уязвимостям веб-приложений.
Цель
Целью этой статьи является предоставление списка лучших практик, которым следует следовать при разработке приложений Node.js.
Рекомендации
Существует несколько рекомендаций по повышению безопасности ваших приложений Node.js. Они классифицируются как:
- Безопасность приложений
- Обработка ошибок и исключений
- Безопасность сервера
- Безопасность платформы
Безопасность приложений
Используйте плоские цепочки обещаний
Асинхронные функции обратного вызова — одна из самых сильных особенностей Node.js. Однако увеличение уровней вложенности внутри функций обратного вызова может стать проблемой. Любой многоэтапный процесс может быть вложен на 10 и более уровней. Эту проблему называют «Пирамидой Судьбы» или «Адом обратных вызовов». В таком коде ошибки и результаты теряются при обратном вызове. Промисы — хороший способ написать асинхронный код, не вдаваясь в вложенные пирамиды. Промисы обеспечивают выполнение сверху вниз, оставаясь при этом асинхронными, передавая ошибки и результаты следующим .then функция.
Еще одним преимуществом Promises является то, как Promises обрабатывают ошибки. Если в классе Promise возникает ошибка, она пропускает .then функции и вызывает первый .catch функция, которую он находит. Таким образом, обещания обеспечивают более высокую уверенность в обнаружении и обработке ошибок. В принципе, вы можете заставить весь ваш асинхронный код (кроме эмиттеров) возвращать обещания. Следует отметить, что вызовы Promise также могут стать пирамидой. Чтобы полностью избежать «ада обратных вызовов», следует использовать плоские цепочки обещаний. Если используемый вами модуль не поддерживает обещания, вы можете преобразовать базовый объект в обещание, используя Promise.promisifyAll() функция.
Следующий фрагмент кода является примером «Ада обратного вызова»:
function func1(name, callback) {
// operations that takes a bit of time and then calls the callback
}
function func2(name, callback) {
// operations that takes a bit of time and then calls the callback
}
function func3(name, callback) {
// operations that takes a bit of time and then calls the callback
}
function func4(name, callback) {
// operations that takes a bit of time and then calls the callback
}
func1("input1", function(err, result1){
if(err){
// error operations
}
else {
//some operations
func2("input2", function(err, result2){
if(err){
//error operations
}
else{
//some operations
func3("input3", function(err, result3){
if(err){
//error operations
}
else{
// some operations
func4("input 4", function(err, result4){
if(err){
// error operations
}
else {
// some operations
}
});
}
});
}
});
}
});Приведенный выше код можно безопасно написать следующим образом, используя плоскую цепочку обещаний:
function func1(name) {
// operations that takes a bit of time and then resolves the promise
}
function func2(name) {
// operations that takes a bit of time and then resolves the promise
}
function func3(name) {
// operations that takes a bit of time and then resolves the promise
}
function func4(name) {
// operations that takes a bit of time and then resolves the promise
}
func1("input1")
.then(function (result){
return func2("input2");
})
.then(function (result){
return func3("input3");
})
.then(function (result){
return func4("input4");
})
.catch(function (error) {
// error operations
});И используя async/await:
async function func1(name) {
// operations that takes a bit of time and then resolves the promise
}
async function func2(name) {
// operations that takes a bit of time and then resolves the promise
}
async function func3(name) {
// operations that takes a bit of time and then resolves the promise
}
async function func4(name) {
// operations that takes a bit of time and then resolves the promise
}
(async() => {
try {
let res1 = await func1("input1");
let res2 = await func2("input2");
let res3 = await func3("input2");
let res4 = await func4("input2");
} catch(err) {
// error operations
}
})();Установить ограничения на размер запроса
Буферизация и анализ тел запросов могут оказаться ресурсоемкой задачей. Если нет ограничений на размер запросов, злоумышленники могут отправлять запросы с большими телами запросов, которые могут исчерпать память сервера и/или заполнить дисковое пространство. Вы можете ограничить размер тела запроса для всех запросов, используя сырое тело.
const contentType = require('content-type')
const express = require('express')
const getRawBody = require('raw-body')
const app = express()
app.use(function (req, res, next) {
if (!['POST', 'PUT', 'DELETE'].includes(req.method)) {
next()
return
}
getRawBody(req, {
length: req.headers['content-length'],
limit: '1kb',
encoding: contentType.parse(req).parameters.charset
}, function (err, string) {
if (err) return next(err)
req.text = string
next()
})
})Однако фиксирование ограничения размера запроса для всех запросов может быть неправильным поведением, поскольку некоторые запросы могут иметь большую полезную нагрузку в теле запроса, например при загрузке файла. Кроме того, ввод типа JSON более опасен, чем составной ввод, поскольку анализ JSON является блокирующей операцией. Поэтому вам следует установить ограничения на размер запроса для разных типов контента. Вы можете сделать это очень легко с помощью экспресс-промежуточного программного обеспечения следующим образом:
app.use(express.urlencoded({ extended: true, limit: "1kb" }));
app.use(express.json({ limit: "1kb" })); Следует отметить, что злоумышленники могут изменить Content-Type заголовок запроса и обойти ограничения размера запроса. Поэтому перед обработкой запроса данные, содержащиеся в запросе, должны быть проверены на соответствие типу контента, указанному в заголовках запроса. Если проверка типа контента для каждого запроса серьезно влияет на производительность, вы можете проверять только определенные типы контента или запрашивать размер, превышающий заранее определенный.
Не блокируйте цикл событий
Node.js сильно отличается от распространенных платформ приложений, использующих потоки. Node.js имеет однопоточную архитектуру, управляемую событиями. Благодаря такой архитектуре пропускная способность становится высокой, а модель программирования упрощается. Node.js реализован на основе неблокирующего цикла событий ввода-вывода. Благодаря этому циклу событий нет ожидания ввода-вывода или переключения контекста. Цикл событий ищет события и отправляет их функциям-обработчикам. По этой причине при выполнении операций JavaScript с интенсивным использованием ЦП цикл событий ожидает их завершения. Именно поэтому такие операции называются «блокировками». Чтобы решить эту проблему, Node.js позволяет назначать обратные вызовы событиям с блокировкой ввода-вывода. Таким образом, основное приложение не блокируется, а обратные вызовы выполняются асинхронно. Поэтому, как правило, все операции блокировки должны выполняться асинхронно, чтобы цикл событий не блокировался.
Даже если вы выполняете операции блокировки асинхронно, ваше приложение все равно может работать не так, как ожидалось. Это происходит, если есть код вне обратного вызова, который полагается на код внутри обратного вызова для запуска первым. Например, рассмотрим следующий код:
const fs = require('fs');
fs.readFile('/file.txt', (err, data) => {
// perform actions on file content
});
fs.unlinkSync('/file.txt'); В приведенном выше примере unlinkSync Функция может выполняться перед обратным вызовом, который удалит файл до того, как будут выполнены желаемые действия с содержимым файла. Такие условия гонки также могут повлиять на безопасность вашего приложения. Примером может служить сценарий, в котором проверка подлинности выполняется в обратном вызове, а действия с проверкой подлинности выполняются синхронно. Чтобы исключить такие условия гонки, вы можете написать все операции, которые полагаются друг на друга, в одной неблокирующей функции. Поступая так, вы можете гарантировать, что все операции выполняются в правильном порядке. Например, приведенный выше пример кода можно записать неблокирующим способом следующим образом:
const fs = require('fs');
fs.readFile('/file.txt', (err, data) => {
// perform actions on file content
fs.unlink('/file.txt', (err) => {
if (err) throw err;
});
});В приведенном выше коде вызов для отключения файла и другие файловые операции находятся в одном обратном вызове. Это обеспечивает правильный порядок действий.
Выполнить проверку ввода
Проверка входных данных — важнейшая часть безопасности приложения. Сбои проверки входных данных могут привести ко многим типам атак на приложения. К ним относятся SQL-инъекция, межсайтовый скриптинг, внедрение команд, включение локальных/удаленных файлов, отказ в обслуживании, обход каталогов, внедрение LDAP и многие другие атаки-инъекции. Чтобы избежать этих атак, входные данные вашего приложения должны быть сначала очищены. Лучший метод проверки входных данных — использовать список принятых входных данных. Однако, если это невозможно, сначала следует проверить входные данные на соответствие ожидаемой схеме ввода и экранировать опасные входные данные. Чтобы упростить проверку ввода в приложениях Node.js, существуют некоторые модули, такие как валидатор и экспресс-монго-дезинфекция. Подробную информацию о проверке ввода см. Статья по проверке ввода.
JavaScript — это динамический язык, и в зависимости от того, как платформа анализирует URL-адрес, данные, видимые кодом приложения, могут принимать разные формы. Вот несколько примеров после анализа строки запроса в express.js:
| URL-адрес | Содержимое request.query.foo в коде |
|---|---|
?foo=bar |
'bar' (нить) |
?foo=bar&foo=baz |
['bar', 'baz'] (массив строк) |
?foo[]=bar |
['bar'] (массив строк) |
?foo[]=bar&foo[]=baz |
['bar', 'baz'] (массив строк) |
?foo[bar]=baz |
{ bar : 'baz' } (предмет с ключом) |
?foo[]baz=bar |
['bar'] (массив строк — постфикс потерян) |
?foo[][baz]=bar |
[ { baz: 'bar' } ] (массив объектов) |
?foo[bar][baz]=bar |
{ foo: { bar: { baz: 'bar' } } } (дерево объектов) |
?foo[10]=bar&foo[9]=baz |
[ 'baz', 'bar' ] (массив строк – порядок уведомления) |
?foo[toString]=bar |
{} (объект, куда вызывается toString() провалится) |
Выполнить экранирование вывода
Помимо проверки ввода, вам следует избегать всего содержимого HTML и JavaScript, показываемого пользователям через приложение, чтобы предотвратить атаки с использованием межсайтовых сценариев (XSS). Вы можете использовать escape-html или узел-эсапи библиотеки для выполнения экранирования вывода.
Ведение журнала активности приложения
Регистрация активности приложений является рекомендуемой хорошей практикой. Это упрощает отладку любых ошибок, возникающих во время выполнения приложения. Это также полезно для обеспечения безопасности, поскольку его можно использовать во время реагирования на инциденты. Кроме того, эти журналы можно использовать в системах обнаружения и предотвращения вторжений (IDS/IPS). В Node.js есть такие модули, как Уинстон, Буньян, или Пино для ведения журнала активности приложения. Эти модули обеспечивают потоковую передачу и запросы журналов, а также позволяют обрабатывать неперехваченные исключения.
С помощью следующего кода вы можете регистрировать действия приложения как в консоли, так и в желаемом файле журнала:
const logger = new (Winston.Logger) ({
transports: [
new (winston.transports.Console)(),
new (winston.transports.File)({ filename: 'application.log' })
],
level: 'verbose'
});Вы можете предоставить разные транспорты, чтобы сохранять ошибки в отдельный файл журнала, а общие журналы приложений — в другой файл журнала. Дополнительную информацию о ведении журнала безопасности можно найти в Статья по ведению журналов.
Мониторинг цикла событий
Когда ваш сервер приложений находится под интенсивным сетевым трафиком, он может быть не в состоянии обслуживать своих пользователей. По сути, это тип Отказ в обслуживании (DoS) атака. слишком занят-js Модуль позволяет отслеживать цикл событий. Он отслеживает время ответа, и когда оно превышает определенный порог, этот модуль может указывать на то, что ваш сервер слишком занят. В этом случае вы можете прекратить обработку входящих запросов и отправить их.503 Server Too Busy сообщение, чтобы ваше приложение оставалось отзывчивым. Пример использования слишком занят-js модуль показан здесь:
const toobusy = require('toobusy-js');
const express = require('express');
const app = express();
app.use(function(req, res, next) {
if (toobusy()) {
// log if you see necessary
res.status(503).send("Server Too Busy");
} else {
next();
}
});Примите меры предосторожности против брутфорса
Брутфорс является общей угрозой для всех веб-приложений. Злоумышленники могут использовать брутфорс в качестве атаки по подбору пароля для получения паролей учетных записей. Поэтому разработчикам приложений следует принимать меры предосторожности против атак методом перебора, особенно на страницах входа. Для этой цели в Node.js имеется несколько модулей. Экспресс-вышибала, экспресс-грубияна и ограничитель скорости это лишь некоторые примеры. В зависимости от ваших потребностей и требований вам следует выбрать один или несколько из этих модулей и использовать их соответствующим образом. Экспресс-вышибала и экспресс-грубияна модули работают аналогично. Они увеличивают задержку для каждого неудачного запроса и могут быть настроены для конкретного маршрута. Эти модули можно использовать следующим образом:
const bouncer = require('express-bouncer');
bouncer.whitelist.push('127.0.0.1'); // allow an IP address
// give a custom error message
bouncer.blocked = function (req, res, next, remaining) {
res.status(429).send("Too many requests have been made. Please wait " + remaining/1000 + " seconds.");
};
// route to protect
app.post("/login", bouncer.block, function(req, res) {
if (LoginFailed){ }
else {
bouncer.reset( req );
}
});const ExpressBrute = require('express-brute');
const store = new ExpressBrute.MemoryStore(); // stores state locally, don't use this in production
const bruteforce = new ExpressBrute(store);
app.post('/auth',
bruteforce.prevent, // error 429 if we hit this route too often
function (req, res, next) {
res.send('Success!');
}
);Кроме экспресс-вышибала и экспресс-грубияна, ограничитель скорости Модуль также может помочь предотвратить атаки грубой силы. Он позволяет указать, сколько запросов может выполнить конкретный IP-адрес в течение определенного периода времени.
const limiter = new RateLimiter();
limiter.addLimit('/login', 'GET', 5, 500); // login page can be requested 5 times at max within 500 secondsИспользование капчи Это также еще один распространенный механизм, используемый против грубого принуждения. Существуют модули, разработанные для Node.js CAPTCHA. Общий модуль, используемый в приложениях Node.js, —SVG-капча. Его можно использовать следующим образом:
const svgCaptcha = require('svg-captcha');
app.get('/captcha', function (req, res) {
const captcha = svgCaptcha.create();
req.session.captcha = captcha.text;
res.type('svg');
res.status(200).send(captcha.data);
});Блокировка аккаунта— рекомендуемое решение, позволяющее держать злоумышленников подальше от ваших действительных пользователей. Блокировка учетной записи возможна с помощью многих модулей, таких как мангуста. Вы можете обратиться к этот пост в блоге чтобы увидеть, как в mongoose реализована блокировка учетной записи.
Используйте токены Anti-CSRF
Подделка межсайтового запроса (CSRF) целью которого является выполнение авторизованных действий от имени аутентифицированного пользователя, при этом пользователь не знает об этом действии. CSRF-атаки обычно выполняются для запросов на изменение состояния, таких как изменение пароля, добавление пользователей или размещение заказов. Ксерфинг— это экспресс-промежуточное программное обеспечение, которое использовалось для смягчения атак CSRF. Но недавно была обнаружена дыра в безопасности этого пакета. Команда, создавшая пакет, не устранила обнаруженную уязвимость и пометила пакет как устаревший, рекомендуя использовать любой другой пакет защиты CSRF.
Подробную информацию об атаках с подделкой межсайтовых запросов (CSRF) и методах их предотвращения можно найти на странице Предотвращение подделки межсайтовых запросов.
Удалить ненужные маршруты
Веб-приложение не должно содержать страниц, которые не используются пользователями, поскольку это может увеличить поверхность атаки приложения. Поэтому все неиспользуемые маршруты API следует отключить в приложениях Node.js. Это происходит особенно в таких средах, как Паруса и Перья, поскольку они автоматически генерируют конечные точки REST API. Например, в Паруса, если URL-адрес не соответствует пользовательскому маршруту, он может соответствовать одному из автоматических маршрутов и все равно генерировать ответ. Такая ситуация может привести к самым разным последствиям: от утечки информации до выполнения произвольной команды. Поэтому перед использованием таких фреймворков и модулей важно знать маршруты, которые они автоматически генерируют, и удалить или отключить эти маршруты.
Предотвращение загрязнения параметров HTTP
Загрязнение параметров HTTP (HPP)— это атака, при которой злоумышленники отправляют несколько параметров HTTP с одним и тем же именем, в результате чего ваше приложение интерпретирует их непредсказуемо. Когда отправляется несколько значений параметров, Express заполняет их в массиве. Чтобы решить эту проблему, вы можете использовать ГЭС модуль. При использовании этот модуль будет игнорировать все значения, представленные для параметра в req.query и/или req.body и просто выберите последнее отправленное значение параметра. Вы можете использовать его следующим образом:
const hpp = require('hpp');
app.use(hpp());Возвращайте только то, что необходимо
Информация о пользователях приложения является одной из наиболее важных сведений о приложении. Таблицы пользователей обычно включают такие поля, как идентификатор, имя пользователя, полное имя, адрес электронной почты, дату рождения, пароль и, в некоторых случаях, номера социального страхования. Поэтому при запросе и использовании пользовательских объектов вам необходимо возвращать только необходимые поля, поскольку это может быть уязвимо для раскрытия личной информации. Это также верно для других объектов, хранящихся в базе данных. Если вам просто нужно определенное поле объекта, вам следует возвращать только определенные требуемые поля. Например, вы можете использовать функцию, подобную следующей, всякий раз, когда вам нужно получить информацию о пользователе. Таким образом, вы сможете вернуть только те поля, которые необходимы для вашей конкретной операции. Другими словами, если вам нужно указать только имена доступных пользователей, вы не возвращаете их адреса электронной почты или номера кредитных карт в дополнение к их полным именам.
exports.sanitizeUser = function(user) {
return {
id: user.id,
username: user.username,
fullName: user.fullName
};
};Используйте дескрипторы свойств объектов
Свойства объекта включают три скрытых атрибута:writable (если false, значение свойства не может быть изменено),enumerable (если false, свойство нельзя использовать в циклах for) и configurable (если false, свойство не может быть удалено). При определении свойства объекта посредством присвоения этим трем скрытым атрибутам по умолчанию присваивается значение true. Эти свойства можно установить следующим образом:
const o = {};
Object.defineProperty(o, "a", {
writable: true,
enumerable: true,
configurable: true,
value: "A"
}); Помимо этого, существует несколько специальных функций для атрибутов объектов.Object.preventExtensions() предотвращает добавление новых свойств к объекту.
Используйте списки контроля доступа
Авторизация не позволяет пользователям действовать за пределами предусмотренных ими разрешений. Для этого необходимо определить пользователей и их роли с учетом принципа наименьших привилегий. Каждая роль пользователя должна иметь доступ только к тем ресурсам, которые она должна использовать. Для ваших приложений Node.js вы можете использовать ACL модуль для реализации ACL (списка контроля доступа). С помощью этого модуля вы можете создавать роли и назначать на эти роли пользователей.
Разрешения
Начиная с Node.js v20, доступна модель разрешений для ограничения привилегий приложения. Начиная с Node.js версии 23.5.0, модель считается стабильной и ее можно включить с помощью --permission флаг (более ранние версии требуют --experimental-permission).
Использование
node --permission [--allow-<type>=...] app.jsПримеры
Используйте только --permission чтобы ограничить все разрешения по умолчанию:
node --permission index.js Использовать --allow‑fs‑read чтобы указать, какие файлы или каталоги Node.js разрешено читать. Это особенно полезно для предотвращения уязвимостей, связанных с включением локальных файлов (LFI):
node --permission --allow-fs-read=/uploads/ index.js Использовать --allow-fs-write чтобы указать, где Node.js разрешено записывать файлы:
node --permission --allow-fs-write=/uploads/ index.jsНесанкционированный доступ вызывает ошибку времени выполнения:
Error: Access to this API has been restricted
code: 'ERR_ACCESS_DENIED'
permission: 'FileSystemRead'
resource: '/path/to/file'Другие флаги:
--allow-child-process— позволяет использоватьchild_processAPI--allow-worker— позволяет использоватьworker_threadsAPI--allow-addons— позволяет загружать собственные аддоны--allow-wasi— позволяет использовать модули WASI
Важное примечание: Символические ссылки выполняются, даже если они указывают за пределы разрешенных путей. Это означает, что относительные символические ссылки могут обходить ограничения и предоставлять непреднамеренный доступ. Чтобы обеспечить безопасность, при использовании модели разрешений убедитесь, что ни один из разрешенных путей не содержит относительных символических ссылок.
Для получения более подробной информации см. официальная документация.
Обработка ошибок и исключений
Обработка неперехваченного исключения
Поведение Node.js для неперехваченных исключений заключается в печати текущей трассировки стека и последующем завершении потока. Однако Node.js позволяет настроить это поведение. Он предоставляет глобальный объект с именем «процесс», доступный всем приложениям Node.js. Это объект EventEmitter, и в случае неперехваченного исключения генерируется событие uncaughtException, которое переносится в основной цикл событий. Чтобы обеспечить собственное поведение для неперехваченных исключений, вы можете привязаться к этому событию. Однако возобновление работы приложения после такого неперехваченного исключения может привести к дальнейшим проблемам. Поэтому, если вы не хотите пропустить ни одно неперехваченное исключение, вам следует выполнить привязку к событию uncaughtException и очистить все выделенные ресурсы, такие как файловые дескрипторы, дескрипторы и т. д., прежде чем завершать процесс. Возобновление работы приложения настоятельно не рекомендуется, поскольку приложение будет находиться в неизвестном состоянии. Важно отметить, что при отображении сообщений об ошибках пользователю в случае неперехваченного исключения подробная информация, такая как трассировка стека, не должна раскрываться пользователю. Вместо этого пользователям должны показываться специальные сообщения об ошибках, чтобы не вызвать утечку информации.
process.on("uncaughtException", function(err) {
// clean up allocated resources
// log necessary error details to log files
process.exit(); // exit the process to avoid unknown state
});Слушайте ошибки при использовании EventEmitter
При использовании EventEmitter ошибки могут возникать в любом месте цепочки событий. Обычно, если в объекте EventEmitter возникает ошибка, вызывается событие ошибки, имеющее объект Error в качестве аргумента. Однако если к этому событию ошибки не подключены прослушиватели, объект Error, отправленный в качестве аргумента, выбрасывается и становится неперехваченным исключением. Короче говоря, если вы не обрабатываете ошибки внутри объекта EventEmitter должным образом, эти необработанные ошибки могут привести к сбою вашего приложения. Поэтому вам всегда следует прослушивать события ошибок при использовании объектов EventEmitter.
const events = require('events');
const myEventEmitter = function(){
events.EventEmitter.call(this);
}
require('util').inherits(myEventEmitter, events.EventEmitter);
myEventEmitter.prototype.someFunction = function(param1, param2) {
//in case of an error
this.emit('error', err);
}
const emitter = new myEventEmitter();
emitter.on('error', function(err){
//Perform necessary error handling here
});Обработка ошибок в асинхронных вызовах
Ошибки, возникающие при асинхронных обратных вызовах, легко не заметить. Поэтому, как правило, первым аргументом асинхронных вызовов должен быть объект Error. Кроме того, экспресс-маршруты сами обрабатывают ошибки, но всегда следует помнить, что ошибки, возникшие в асинхронных вызовах, выполненных внутри экспресс-маршрутов, не обрабатываются, если только объект Error не отправляется в качестве первого аргумента.
Ошибки в этих обратных вызовах могут распространяться столько раз, сколько возможно. Каждый обратный вызов, на который была распространена ошибка, может игнорировать, обрабатывать или распространять ошибку.
Безопасность сервера
Установите флаги cookie соответствующим образом
Обычно информация о сеансе отправляется с использованием файлов cookie в веб-приложениях. Однако неправильное использование файлов cookie HTTP может привести к возникновению в приложении нескольких уязвимостей управления сеансами. Для каждого файла cookie можно установить некоторые флаги, чтобы предотвратить подобные атаки.httpOnly, Secure и SameSite флаги очень важны для файлов cookie сеанса.httpOnly Флаг запрещает доступ к файлу cookie со стороны клиентского JavaScript. Это эффективная мера противодействия XSS-атакам.Secure Флаг позволяет отправлять файлы cookie только в том случае, если связь осуществляется через HTTPS.SameSite Флаг может предотвратить отправку файлов cookie в межсайтовых запросах, что помогает защититься от атак подделки межсайтовых запросов (CSRF). Помимо них, есть и другие флаги, такие как домен, путь и срок действия. Установка этих флагов приветствуется, но они в основном связаны с областью действия файлов cookie, а не с безопасностью файлов cookie. Пример использования этих флагов приведен в следующем примере:
const session = require('express-session');
app.use(session({
secret: 'your-secret-key',
name: 'cookieName',
cookie: { secure: true, httpOnly: true, path: '/user', sameSite: true}
}));Используйте соответствующие заголовки безопасности
Есть несколько Заголовки безопасности HTTP это может помочь вам предотвратить некоторые распространенные векторы атак. шлем package может помочь установить эти заголовки:
const express = require("express");
const helmet = require("helmet");
const app = express();
app.use(helmet()); // Add various HTTP headers Высший уровень helmet Функция представляет собой оболочку 14 меньших промежуточных программ.
Ниже приведен список заголовков безопасности HTTP, охватываемых helmet промежуточное ПО:
- Строгая транспортная безопасность: Строгая транспортная безопасность HTTP (HSTS) указывает браузерам, что доступ к приложению возможен только через HTTPS-соединения. Чтобы использовать его в своем приложении, добавьте следующие коды:
app.use(helmet.hsts()); // default configuration
app.use(
helmet.hsts({
maxAge: 123456,
includeSubDomains: false,
})
); // custom configuration- Параметры X-Frame: определяет, можно ли загрузить страницу через
<frame>или<iframe>элемент. Если разрешить размещение страницы в рамке, это может привести к Кликджекинг атаки.
app.use(helmet.frameguard()); // default behavior (SAMEORIGIN)- X-XSS-Защита: останавливает загрузку страниц при обнаружении атак с использованием отраженного межсайтового скриптинга (XSS). Этот заголовок устарел в современных браузерах, и его использование может создать дополнительные проблемы безопасности на стороне клиента. Таким образом, рекомендуется установить заголовок какX-XSS-Защита: 0 чтобы отключить XSS Auditor и не позволить ему использовать поведение браузера по умолчанию, обрабатывающего ответ.
app.use(helmet.xssFilter()); // sets "X-XSS-Protection: 0"Для современных браузеров рекомендуется реализовать сильный Политика безопасности контента политику, как подробно описано в следующем разделе.
- Политика безопасности контента: Политика безопасности контента разработана для снижения риска таких атак, как Межсайтовый скриптинг (XSS) и Кликджекинг. Это позволяет контент из списка, который вы решите. Он имеет несколько директив, каждая из которых запрещает загрузку определенного типа контента. Вы можете обратиться к Статья по политике безопасности контента для подробного объяснения каждой директивы и того, как ее использовать. Вы можете реализовать эти настройки в своем приложении следующим образом:
app.use(
helmet.contentSecurityPolicy({
// the following directives will be merged into the default helmet CSP policy
directives: {
defaultSrc: ["'self'"], // default value for all directives that are absent
scriptSrc: ["'self'"], // helps prevent XSS attacks
frameAncestors: ["'none'"], // helps prevent Clickjacking attacks
imgSrc: ["'self'", "'http://imgexample.com'"],
styleSrc: ["'none'"]
}
})
);Поскольку это промежуточное программное обеспечение выполняет очень мало проверок, рекомендуется полагаться на средства проверки CSP, такие как Оценщик CSP вместо.
- Параметры X-Content-Type: Даже если сервер устанавливает действительный
Content-Typeв ответе, браузеры могут попытаться определить тип MIME запрошенного ресурса. Этот заголовок позволяет остановить такое поведение и указать браузеру не изменять типы MIME, указанные вContent-Typeзаголовок. Его можно настроить следующим образом:
app.use(helmet.noSniff());- Кэш-Контроль и Прагма: Заголовок Cache-Control можно использовать, чтобы запретить браузерам кэшировать данные ответы. Это следует сделать для страниц, содержащих конфиденциальную информацию о пользователе или о приложении. Однако отключение кэширования для страниц, не содержащих конфиденциальной информации, может серьезно повлиять на производительность приложения. Поэтому кеширование следует отключать только для страниц, возвращающих конфиденциальную информацию. Соответствующие элементы управления кэшированием и заголовки можно легко настроить с помощью нокеш упаковка:
const nocache = require("nocache");
app.use(nocache());Приведенный выше код устанавливает заголовки Cache-Control, Surrogate-Control, Pragma и Expires соответственно.
- X-Параметры загрузки: Этот заголовок не позволяет Internet Explorer выполнять загруженные файлы в контексте сайта. Это достигается с помощью директивы noopen. Вы можете сделать это с помощью следующего фрагмента кода:
app.use(helmet.ieNoOpen());- X-Powered-By: Заголовок X-Powered-By используется для информирования о том, какая технология используется на стороне сервера. Это ненужный заголовок, вызывающий утечку информации, поэтому его следует удалить из вашего приложения. Для этого вы можете использовать
hidePoweredByследующее:
app.use(helmet.hidePoweredBy());Также в этом заголовке можно лгать об используемых технологиях. Например, даже если ваше приложение не использует PHP, вы можете настроить заголовок X-Powered-By так, чтобы он выглядел так.
app.use(helmet.hidePoweredBy({ setTo: 'PHP 4.2.0' }));Безопасность платформы
Держите ваши пакеты в актуальном состоянии
Безопасность вашего приложения напрямую зависит от того, насколько безопасны сторонние пакеты, которые вы используете в своем приложении. Поэтому важно поддерживать пакеты в актуальном состоянии. Следует отметить, что Использование компонентов с известными уязвимостями все еще входит в десятку лучших OWASP. Вы можете использовать Проверка зависимостей OWASP чтобы узнать, имеет ли какой-либо из пакетов, используемых в проекте, известную уязвимость. Кроме того, вы можете использовать Удалить.js для проверки библиотек JavaScript с известными уязвимостями.
Начиная с версии 6,npm представил audit, который предупредит об уязвимых пакетах:
npm auditnpm также представлен простой способ обновления затронутых пакетов:
npm audit fixЕсть несколько других инструментов, которые вы можете использовать для проверки ваших зависимостей. Более полный список можно найти в CS Управление уязвимыми зависимостями.
Не используйте опасные функции
Некоторые функции JavaScript опасны и их следует использовать только там, где это необходимо или неизбежно. Первый пример – это eval() функция. Эта функция принимает строковый аргумент и выполняет его как любой другой исходный код JavaScript. В сочетании с пользовательским вводом такое поведение по своей сути приводит к уязвимости удаленного выполнения кода. Аналогично, вызовы child_process.exec также очень опасны. Эта функция действует как интерпретатор bash и отправляет свои аргументы в /bin/sh. Вводя входные данные в эту функцию, злоумышленники могут выполнять произвольные команды на сервере.
Помимо этих функций, некоторые модули требуют особой осторожности при использовании. В качестве примера:fs модуль обрабатывает операции файловой системы. Однако если в этот модуль подается неправильно обработанный пользовательский ввод, ваше приложение может стать уязвимым для уязвимостей включения файлов и обхода каталогов. Сходным образом,vm Модуль предоставляет API для компиляции и запуска кода в контекстах виртуальной машины V8. Поскольку по своей природе он может выполнять опасные действия, его следует использовать в песочнице.
Было бы несправедливо утверждать, что эти функции и модули вообще не следует использовать, однако использовать их следует осторожно, особенно когда они используются с пользовательским вводом. Также есть некоторые другие функции это может сделать ваше приложение уязвимым.
Держитесь подальше от злых регулярных выражений
Отказ в обслуживании с использованием регулярных выражений (ReDoS) — это атака типа «отказ в обслуживании», в которой используется тот факт, что большинство реализаций регулярных выражений могут достигать экстремальных ситуаций, из-за которых они работают очень медленно (экспоненциально зависит от размера входных данных). Затем злоумышленник может заставить программу, использующую регулярное выражение, войти в эти экстремальные ситуации, а затем зависнуть на очень долгое время.
Отказ в обслуживании с помощью регулярных выражений (ReDoS)— это тип атаки типа «отказ в обслуживании», в которой используются регулярные выражения. Некоторые реализации регулярных выражений (Regex) вызывают экстремальные ситуации, из-за которых приложение работает очень медленно. Злоумышленники могут использовать такие реализации регулярных выражений, чтобы заставить приложение попадать в такие экстремальные ситуации и зависать на долгое время. Такие регулярные выражения называются злыми, если приложение может зависнуть на созданном вводе. Обычно эти регулярные выражения используются для группировки с повторением и чередования с перекрытием. Например, следующее регулярное выражение ^(([a-z])+.)+[A-Z]([a-z])+$ может использоваться для указания имен классов Java. Однако этому регулярному выражению может соответствовать и очень длинная строка (аааа...аааааааааа...аааа). Существует несколько инструментов, позволяющих проверить, может ли регулярное выражение вызвать отказ в обслуживании. Одним из примеров является vuln-regex-детектор.
Запуск линтеров безопасности
При разработке кода учитывать все советы по безопасности может быть очень сложно. Кроме того, заставить всех членов команды соблюдать эти правила практически невозможно. Вот почему существуют инструменты статического анализа безопасности (SAST). Эти инструменты не выполняют ваш код, а просто ищут шаблоны, которые могут содержать угрозы безопасности. Поскольку JavaScript — это динамический язык со свободной типизацией, инструменты линтинга действительно важны в жизненном цикле разработки программного обеспечения. Правила линтинга следует периодически пересматривать, а результаты проверять. Еще одним преимуществом этих инструментов является возможность добавления собственных правил для шаблонов, которые могут показаться вам опасными.ESLint и JSHint— это обычно используемые инструменты SAST для проверки JavaScript.
Использовать строгий режим
В JavaScript имеется ряд небезопасных и опасных устаревших функций, которые не следует использовать. Чтобы удалить эти функции, ES5 включил строгий режим для разработчиков. В этом режиме выдаются ошибки, о которых раньше не сообщалось. Это также помогает движкам JavaScript выполнять оптимизацию. В строгом режиме ранее принятый неправильный синтаксис приводит к реальным ошибкам. Из-за этих улучшений вам всегда следует использовать строгий режим в своем приложении. Для того, чтобы включить строгий режим, вам просто нужно написать "use strict"; поверх вашего кода.
Следующий код сгенерирует ReferenceError: Can't find variable: y на консоли, которая не будет отображаться, если не используется строгий режим:
"use strict";
func();
function func() {
y = 3.14; // This will cause an error (y is not defined)
}Соблюдайте общие принципы безопасности приложений.
Этот список в основном посвящен проблемам, которые часто встречаются в приложениях Node.js, с рекомендациями и примерами. Помимо них, существуют общие безопасность по принципам проектирования которые применяются к веб-приложениям независимо от технологий, используемых на сервере приложений. Вам также следует помнить об этих принципах при разработке приложений. Вы всегда можете обратиться к Серия статей OWASP чтобы узнать больше об уязвимостях веб-приложений и методах их устранения, используемых против них.
Дополнительные ресурсы о безопасности Node.js
© Перевод на русский язык. Оригинал: OWASP Node.js Security Cheat Sheet и серия OWASP Cheat Sheet Series.
Этот проект использует материалы OWASP, распространяемые по лицензии CC BY-SA 4.0.