23 March

Шпаргалки по безопасности: Nodejs

Акрибия corporate blogInformation SecurityWebsite developmentNode.JS


Довольно много уже было сказано о популярности NodeJS. Рост количества приложений очевиден – NodeJS довольно прост в освоении, имеет огромное количество библиотек, а также динамично развивающуюся экосистему.

Мы подготовили рекомендации для NodeJS разработчиков, основываясь на OWASP Cheat Sheets, которые помогут вам предусмотреть проблемы с безопасностью при разработке приложений.

Рекомендации к безопасности NodeJS приложений можно разделить на следующие категории:

  • Безопасность при разработке приложения;
  • Безопасность сервера;
  • Безопасность платформы;


Безопасность при разработке приложения


Избегайте callback hell

Использование функций обратных вызовов (коллбэков) — одна из самых сильных сторон NodeJS, однако при вложенности коллбэков можно легко забыть обработать ошибку в одной из функций. Один из способов избежать callback hell — использование промисов. Даже если используемый вами модуль не поддерживает работу с промисами, вы всегда можете использовать Promise.promisifyAll(). Но даже используя промисы, стоит обращать внимание на вложенность. Чтобы полностью избежать ошибки callback hell придерживайтесь «плоской» цепочки промисов.

Пример callback hell:

function func1(name, callback) {
   setTimeout(function() {
      // operations
   }, 500);
}
function func2(name, callback) {
   setTimeout(function() {
      // operations
   }, 100);
}
function func3(name, callback) {
   setTimeout(function() {
      // operations
   }, 900);
}
function func4(name, callback) {
   setTimeout(function() {
      // operations
   }, 3000);
}

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, callback) {
   setTimeout(function() {
      // operations
   }, 500);
}
function func2(name, callback) {
   setTimeout(function() {
      // operations
   }, 100);
}
function func3(name, callback) {
   setTimeout(function() {
      // operations
   }, 900);
}
function func4(name, callback) {
   setTimeout(function() {
      // operations
   }, 3000);
}

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
   });

Ограничивайте размер запроса

Разбор тела запроса может оказаться довольно ресурсоемкой операцией. Если не ограничивать размер запроса, злоумышленники смогут отправлять достаточно большие запросы, способные заполнить все дисковое пространство или исчерпать все ресурсы сервера, но в то же время ограничение размера запроса для всех случаев может быть некорректным, так как существуют запросы, как например загрузка файла. Поэтому рекомендуется устанавливать ограничения для разных типов контента. Например, с помощью фреймфорка express это можно реализовать следующим образом:

app.use(express.urlencoded({ limit: "1kb" }));
app.use(express.json({ limit: "1kb" }));
app.use(express.multipart({ limit:"10mb" }));

Следует заметить, что злоумышленник может изменить тип содержимого запроса и обойти ограничения, поэтому необходимо проверять соответствие содержимого запроса на соответствие типу контента, указанному в заголовке запроса. Если проверка типа содержимого влияет на производительность, то можно проверять только определенные типы или запросы, размер которых превышает определенный размер.

Не блокируйте event loop

Важная составляющая языка — event loop, который как раз и позволяет переключать контекст выполнения, не дожидаясь окончания выполнения операции. Однако существуют блокирующие операции, окончания выполнения которых NodeJS приходится дожидаться, прежде чем продолжить выполнение кода. Например, большинство синхронных методов относятся к блокирующим:

const fs = require('fs');
fs.unlinkSync('/file.txt');

Рекомендуется выполнять подобные операции асинхронно:

const fs = require('fs');
fs.unlink('/file.txt', (err) => {
    if (err) throw err;
});

При этом не забывайте, что код, стоящий после асинхронного вызова будет выполнен, не дожидаясь окончания выполнения предыдущей операции.

Например, в коде, приведенном ниже, файл удалится до того, как он будет прочтен, что может привести к race condition.

const fs = require('fs');
fs.readFile('/file.txt', (err, data) => {
  // perform actions on file content
});
fs.unlinkSync('/file.txt');

Чтобы избежать этого, можно записать все операции в неблокирующую функцию:

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-инъекции, xss, command injection и другим. Чтобы упростить проверку форм можно воспользоваться пакетами validator, mongo-express-sanitize.

Экранируйте пользовательские данные

Одно из правил, выполнение которого поможет вам защититься от xss атак — экранирование пользовательских данных. Для этого можно воспользоваться библиотекой escape-html или node-esapi.

Ведите логи

Кроме того, что это поможет при отладке ошибок, логирование может использоваться для реагирования на инциденты. Подробнее о необходимости логирования вы можете почитать тут. Одни из самых популярных пакетов для логирования в NodeJS — Winston и Bunyan. В примере ниже показано как с помощью Winston выводить логи как в консоль, так и в файл:

var logger = new (Winston.Logger) ({
    transports: [
        new (winston.transports.Console)(),
        new (winston.transports.File)({ filename: 'application.log' })
    ],
    level: 'verbose'
});

Контролируйте цикл событий

Если ваш сервер находится в условиях интенсивного сетевого трафика, пользователи могу испытывать сложности с доступностью вашего сервиса. По сути, это атака типа DoS. В таком случае вы можете отслеживать время отклика и, если оно превышает заданное, отправлять сообщение 503 Server Too Busy. Помочь в этом может модуль toobusy-js.

Пример использования модуля:

var toobusy = require('toobusy-js');
var express = require('express');
var app = express();
app.use(function(req, res, next) {
    if (toobusy()) {
        // log if you see necessary
        res.send(503, "Server Too Busy");
    } else {
    next();
    }
});

Примите меры предосторожности против брут-форса

Опять же на помощь приходят модули. Например, express-brute или express-bouncer. Пример использования:

var bouncer = require('express-bouncer');
bouncer.whitelist.push('127.0.0.1'); // whitelist an IP address
// give a custom error message
bouncer.blocked = function (req, res, next, remaining) {
    res.send(429, "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 );
    }
});

Использование CAPTCHA является еще одним распространенным механизмом противодействия брут-форсу. Часто используемый модуль, помогающий реализовать CAPTCHA — svg-captcha.

Используйте CSRF токены

Один из самых надежных способов защиты от CSRF атак — использование CSRF токена. Токен должен быть сгенерирован с высокой энтропией, строго проверяться и быть привязанным к сеансу пользователя. Для обеспечения работы CSRF токена можно воспользоваться модулем csurf.

Пример использования:

var csrf = require('csurf');
csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, function(req, res) {
    res.render('send', { csrfToken: req.csrfToken() })
})
app.post('/process', parseForm, csrfProtection, function(req, res) {
    res.send('data is being processed');
});

Не забудьте добавить токен в скрытое поле на странице:

<input type="hidden" name="_csrf" value="{{ csrfToken }}">

Более подробно о CSRF токенах можно прочесть в нашей статье.

Удаляйте ненужные роуты

Веб-приложение не должно содержать страниц, которые не используются пользователями, поскольку это может увеличить поверхность атаки. Поэтому все неиспользуемые API роуты должны быть отключены. Особенно стоит обратить внимание на этот вопрос, если вы используете фреймфорки Sails или Feathers, поскольку они автоматически генерируют API эндпоинты.

Защититесь от HPP (HTTP Parameter Pollution)

По умолчанию express складывает все параметры из запроса в массив. OWASP рекомендует использовать модуль hpp, который игнорирует все значения параметров из req.query и/или req.body и просто выбирает последнее значение среди повторяющихся.

var hpp = require('hpp');
app.use(hpp());

Контролируйте возвращаемые значения

К примеру, в таблице пользователей могут храниться важные данные: пароль, адрес электронной почты, дата рождения и т.д. Поэтому важно возвращать только необходимые данные.

Например:

 exports.sanitizeUser = function(user) {
  return {
    id: user.id,
    username: user.username,
    fullName: user.fullName
  };
};

Используйте дескрипторы

С помощью дескрипторов можно описать поведение свойства для различных операций: writable — можно ли менять значение свойства, enumerable — можно ли использовать свойство в цикле for..in, configurable — можно ли перезаписывать свойство. Рекомендуется обращать внимание на перечисленные свойства, так как при определении свойства объекта все эти атрибуты имеют значение true по умолчанию. Изменить значение свойств можно следующим образом:

var o = {};
Object.defineProperty(o, "a", {
    writable: true,
    enumerable: true,
    configurable: true,
    value: "A"
});

Используйте ACL

Для разграничения доступа к данным с учетом ролей может помочь модуль acl. Например, добавление разрешения выглядит следующим образом:

// guest is allowed to view blogs
acl.allow('guest', 'blogs', 'view')
// allow function accepts arrays as any parameter
acl.allow('member', 'blogs', ['edit', 'view', 'delete'])

Отлавливайте uncaughtException

По умолчанию, в случае неперехваченного исключения NodeJS выведет текущий stack trace и завершит поток выполнения. Однако NodeJS позволяет настроить это поведение. В случае появления неперехваченного исключения генерируется событие uncaughtException, которое можно отловить с помощью объекта process:

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
});

Стоит помнить, что при возникновении uncaughtException необходимо очистить все выделенные ресурсы (например, файловые дескрипторы и хэндлеры) перед завершением Z процесса, чтобы избежать возникновения непредвиденных ошибок. Настоятельно не рекомендуется продолжать работу программы в случае возникновения uncaughtException.

Также при отображении сообщений об ошибках пользователю не следует раскрывать подробную информацию об ошибке, такую как stack trace.

Безопасность сервера


Устанавливайте флаги для заголовков при работе с куками

Есть несколько флагов, использование которых поможет защититься от таких атак как xss и csrf: httpOnly, запрещающий доступ к кукам посредством javascript; Secure — позволяет отправку куки только по HTTPS и SameSite, определяющий возможность передачи куки стороннему ресурсу.

Пример использования:

var session = require('express-session');
app.use(session({
    secret: 'your-secret-key',
    key: 'cookieName',
    cookie: { secure: true, httpOnly: true, path: '/user', sameSite: true}
}));

Устанавливайте HTTP-заголовки для обеспечения безопасности

Ниже приведены заголовки и примеры их подключения, которые помогут вам защититься от ряда распространённых атак. Установка заголовков выполняется с помощью модуля helmet

• Strict-Transport-Security: HTTP Strict Transport Security (HSTS) сообщает браузеру о том, что приложение может быть доступно только по HTTPS

app.use(helmet.hsts()); // default configuration
app.use(helmet.hsts("<max-age>", "<includeSubdomains>")); // custom configuration

• X-Frame-Options: определяет, может ли страница использоваться в frame, iframe, embed или object

app.use(hemlet.xframe()); // default behavior (DENY)
helmet.xframe('sameorigin'); // SAMEORIGIN
helmet.xframe('allow-from', 'http://alloweduri.com'); //ALLOW-FROM uri

• X-XSS-Protection: позволяет браузеру прекращать загрузку страницы, если он обнаружил отраженную XSS атаку.

var xssFilter = require('x-xss-protection');
app.use(xssFilter());

• X-Content-Type-Options: используется для предотвращения атаки с использованием MIME типов

app.use(helmet.noSniff());

• Content-Security-Policy: позволяет предотвращать такие атаки как XSS и атаки внедрения данных

const csp = require('helmet-csp')
app.use(csp({
   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'"]
    }
}))

• Cache-Control и Pragma: для управления кэшированием, особенно этот заголовок может быть полезен для страниц, которые содержат конфиденциальные данные. Однако стоит помнить, что отключение кэширования на всех страницах может сказаться на производительности

app.use(helmet.noCache());

• X-Download-Options: заголовок запрещает Inter Explorer исполнять скаченные файлы

app.use(helmet.ieNoOpen());

• Expect-CT: Certificate Transparency — механизм, созданный, чтобы решить некоторые проблемы с инфраструктурой SSL сертификатов, данный заголовок сообщает браузеру о необходимости дополнительной проверки сертификата в логах CT

var expectCt = require('expect-ct');
app.use(expectCt({ maxAge: 123 }));
app.use(expectCt({ enforce: true, maxAge: 123 }));
app.use(expectCt({ enforce: true, maxAge: 123, reportUri: 'http://example.com'}));

• X-Powered-By: необязательный заголовок, который используется, чтобы указать технологию, используемую на сервере. Скрыть данный заголовок можно следующим образом:

app.use(helmet.hidePoweredBy());

Кроме того, можно подменить значение, чтобы скрыть реальную информацию об используемых вами технологиях:

app.use(helmet.hidePoweredBy({ setTo: 'PHP 4.2.0' }));

Безопасность платформы


Обновляйте ваши пакеты

Безопасность вашего приложения зависит в том числе и от безопасности используемых вами пакетов, поэтому важно пользоваться последней версией пакета. Чтобы убедиться, что используемый вами пакет не содержит известных уязвимостей можно воспользоваться специальным списком OWASP. Также можно воспользоваться библиотекой, выполняющей проверку пакетов на известные уязвимости Retire.js.

Не используйте небезопасные функции

Существуют функции, от использования которых по возможности рекомендуется отказаться. Среди таких функций — eval(), исполняющая принимаемую в качестве аргумента строку. В сочетании с пользовательским вводом использование этой функции может привести к уязвимости удаленного выполнения кода, так как по схожим причинам использование child_process.exec также является небезопасным, так как функция передает принятые аргументы в bin/sh.

Кроме того, существует ряд модулей, пользоваться которыми стоит с осторожностью. Например, модуль fs для работы с файлами. Если определенным образом сформированный пользовательский ввод подать в функцию, то ваше приложение может стать уязвимым к включению локального файла и directory traversal.

Модуль vm, предоставляющий API для компиляции и запуска кода на виртуальной машинеV8, стоит использовать только в песочнице.

Здесь можно ознакомиться с другими функциями, которые могут сделать ваше приложение небезопасным

Будьте осторожны, используя регулярные выражения

Регулярное выражение может быть написано так, что можно добиться ситуации, когда время работы выражения будет расти экспоненциально, что может привести к отказу в обслуживании. Подобные атаки называются ReDoS. Есть несколько инструментов, позволяющих проверить, является ли безопасным регулярные выражение, одно из таких — vuln-regex-detector.

Периодически запускайте линтеры

Во время разработки тяжело удерживать в голове все рекомендации по обеспечению безопасности, а если речь идет о командной разработке, непросто добиться соблюдения правил всеми членами команды. Для таких целей и существуют инструменты для статического анализа безопасности. Такие инструменты, не выполняя ваш код ищут в нем уязвимости. Кроме того, линтеры позволяют добавлять пользовательские правила поиска мест в коде, которые могут быть уязвимы. Самые часто используемые линтеры — ESLint и JSHint.

Используйте strict mode

Javascript имеет ряд небезопасных и устаревших функций, которые не должны использоваться. Чтобы исключить возможность использования этих функций и предусмотрен strict mode.

Придерживайтесь общих принципов безопасности

В описанных рекомендациях основной упор делается на NodeJS, но не стоит забывать про общие принципы безопасности, которые необходимо соблюдать независимо от используемой платформы.
Tags:информационная безопасностьразработкабезопасная разработкаnodejs
Hubs: Акрибия corporate blog Information Security Website development Node.JS
+15
7.6k 175
Comments 2
Top of the last 24 hours