Статьи

Защита Node.js: управление сессиями в Express.js

Каждое взаимодействие пользователя с вашим приложением — это отдельный и индивидуальный запрос и ответ. Необходимость сохранять информацию между запросами жизненно важна для обеспечения максимального удобства для пользователя, и то же самое относится и к веб-приложению Node.js, такому как популярная среда Express.js.

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

В последней статье « Обеспечение безопасности Node.js» мы рассмотрели требования к учетным записям пользователей. Точно так же мы рассмотрим, как мы можем безопасно настроить сеансы в нашем приложении для снижения рисков, таких как перехват сеансов. Мы собираемся посмотреть, как мы можем скрыть идентификаторы сеансов, установить время жизни в наших сеансах, настроить безопасные файлы cookie для транспортировки сеансов и, наконец, важность и роль безопасности транспортного уровня (TLS), когда дело доходит до используя сеансы.

Установка сессий в Express.js

Мы собираемся использовать экспресс-сессии модуля NPM , очень популярный модуль сессий, который был тщательно проверен сообществом и постоянно совершенствуется.  

Мы передадим объект нашего экспресс-приложения функции, чтобы подключить модуль экспресс-сессии:

"use strict";
// provides a promise to a mongodb connection
import connectionProvider            from "../data_access/connectionProvider";
// provides application details such as MongoDB URL and DB name
import {serverSettings}              from "../settings";
import session                       from "express-session";
import mongoStoreFactory             from "connect-mongo";

export default function sessionManagementConfig(app) {

    // persistence store of our session
    const MongoStore = mongoStoreFactory(session);

    app.use(session({
        store: new MongoStore({
            dbPromise: connectionProvider(serverSettings.serverUrl, serverSettings.database)
        }),
        secret: serverSettings.session.password,
        saveUninitialized: true,
        resave: false,
        cookie: {
            path: "/",
        }
    }));

    session.Session.prototype.login = function(user, cb){
        this.userInfo = user;
        cb();
    };
}

Что тут происходит

Мы импортируем функцию сеанса из модуля NPM экспресс-сеанса и передаем функции сеанса объект конфигурации для установки таких свойств, как:

  • Хранить. Я использую MongoDB в качестве бэкэнда и хочу сохранить сеансы приложений в моей базе данных, поэтому я использую модуль connect-mongo NPM и устанавливаю значение хранилища сеансов для экземпляра этого модуля. Однако вы можете использовать другой бэкэнд, поэтому ваш магазин может отличаться. По умолчанию для экспресс-сессии используется хранилище в памяти.

  • Секрет.  Это значение используется при подписании идентификатора сеанса, который хранится в файле cookie.

  • Cookie.  Это определяет поведение файла cookie HTTP, в котором хранится идентификатор сеанса.

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

Session Hijacking

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

Идентификация сессии

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

В случае наших сеансов контрольным знаком является имя файла cookie сеанса connect.sid, который может помочь злоумышленнику определить используемый механизм сеанса и найти конкретные уязвимости.  

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

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

 app.use(session({
        store: new MongoStore({
            dbPromise: connectionProvider(serverSettings.serverUrl, serverSettings.database)
        }),
        secret: serverSettings.session.password,
        saveUninitialized: true,
        resave: false,
        cookie: {
            path: "/",
        }

       name: "id"
    }));

Что мы сделали?

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

Теперь, когда мы предоставили некоторую степень запутанности для наших сессий, давайте посмотрим, как мы можем уменьшить окно возможностей для перехвата сессий.

Время жизни сессии

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

Таким образом, мы можем уменьшить окно, в течение которого сеанс активен, и напрямую повлиять на вероятность того, что злоумышленник может использовать пользователя или систему из захваченного сеанса, ограничивая время жизни сеанса.

Как я упоминал ранее,  модуль NPM экспресс-сессий предоставляет свойство хранилища, где вы можете установить отдельный механизм хранения для хранения ваших сессий (по умолчанию это in-memory). Следовательно, следующее изменение связано с вашим внутренним хранилищем сессий. В моем случае я храню свои сеансы в базе данных MongoDB и использую модуль NPM connect-mongo для простого хранения сеансов в базе данных.   

В этом случае я могу предоставить свойству ttl значение в секундах в объекте конфигурации, предоставленном в MongoStore, по умолчанию это 14 дней (14 * 24 * 60 * 60):

app.use(session({
   store: new MongoStore({
       dbPromise: connectionProvider(serverSettings.serverUrl, serverSettings.database),
       ttl: (1 * 60 * 60)
   }),
   //remaining - removed for brevity
}));

Cookie Time-to-Live

Не так важно, как TTL сеанса, мы также можем установить срок действия файла cookie, который используется для передачи идентификатора сеанса, в объекте конфигурации сеанса.
Мы можем предоставить свойство и значение maxAge в миллисекундах в объекте cookie .

app.use(session({
       //..previous removed for brevity

        cookie: {
            path: "/“,
       maxAge:  1800000  //30 mins
        }
}));

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

Совет: самая важная вещь — это продолжительность сеанса, поэтому, независимо от того, устанавливаете ли вы возраст файла cookie, вы никогда не должны полагаться на него отдельно и всегда должны регулировать время существования сеанса. Один из способов уравновесить пользовательский опыт — потребовать повторной аутентификации во время попытки пользователя получить доступ к ключевым областям доступа вашего сайта.

Регенерация новых сессий

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

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

Вы заметите, что в исходной функции, где мы подключили наши Sessions, мы добавляем метод Login к прототипу Session, чтобы он был доступен для всех экземпляров объекта Session.  

session.Session.prototype.login = function(user){
        this.userInfo = user;
};

Это просто удобный метод, к которому я могу получить доступ, чтобы связать информацию пользователя с сеансом пользователя, например, в случае API-интерфейса маршрутизации аутентификации. Например, как мы видели в Node.js и хранилище паролей с помощью Bcrypt, после успешного входа в систему мы имеем доступ к объекту сеанса вне запроса.

authenticationRouter.route("/api/user/login")
    .post(async function (req, res) {
        try {

          //removed for brevity….

          req.session.login(userInfo, function(err) {
            if (err) {
               return res.status(500).send("There was an error logging in. Please try again later.");
            }
          });
        //removed for brevity….
     });

Однако крайне важно не продолжать использовать тот же идентификатор сеанса после успешной аутентификации пользователя. Модуль экспресс-сеансов предоставляет удобный метод регенерации для регенерации идентификатора сеанса. Наш метод loginprototype вне объекта Session является удобным местом для регенерации идентификатора Session, поэтому давайте обновим:

session.Session.prototype.login = function (user, cb) {
   const req = this.req;
   req.session.regenerate(function(err){
      if (err){
         cb(err);
      }
   });

   req.session.userInfo = user;
   cb();
};

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

Поскольку cookie-файлы — это то, что мы используем для транспортировки наших сеансов, важно внедрить безопасные cookie-файлы. Давайте посмотрим, как мы можем сделать это дальше.

Печенье

Мы используем сеансы для поддержания состояния между запросами пользователей, и мы используем куки для передачи идентификатора сеанса между этими запросами. Вы, вероятно, не ездите по темным окрестностям, не заперев свои двери, и при этом вы не должны выбрасывать свои сессии в дикой природе без какой-либо защиты.  

Есть в первую очередь 3 способа защиты cookie, 2, которые мы рассмотрим здесь, и третий, который мы рассмотрим в следующем разделе, когда рассмотрим обслуживание контента нашего приложения через HTTPS.

HTTPOnly Flag

Поскольку идентификатор сеанса бесполезен для клиента, нет абсолютно никаких причин, по которым интерфейсное приложение должно иметь доступ к идентификатору сеанса в файле cookie. Однако по умолчанию любой скрипт, работающий на внешнем приложении, будет иметь доступ к содержимому куки.

Мы можем ограничить доступ к содержимому нашего сеансового куки, выполнив заголовок HTTP Set-cookie и указав флаг HTTPOnly для нашего сеансового куки.   

Заголовок HTTP:

Set-cookie: mycookie=value; path=/; HttpOnly

Мы можем легко предоставить этот заголовок, просто установив свойство httpOnly в нашем объекте cookie на «true:»

app.use(session({
       //..previous removed for brevity

        cookie: {
            path: “/“,
            httpOnly: true,
       maxAge:  1800000
        }
}));

Теперь только агент (т. Е. Браузер) будет иметь доступ к cookie-файлу, чтобы повторно отправить его при следующем запросе к тому же домену. Это напрямую поможет смягчить угрозы межсайтового скриптинга, которые в противном случае могли бы получить доступ к содержимому нашего файла cookie сеанса.

Безопасный флаг

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

Атаки «человек посередине» (MitM) распространены и могут быть легко выполнены любым, кто имеет доступ к сетевому трафику. Это относится к любой локальной кофейне WIFI, которую обычно могут использовать пользователи. Если информация о вашем сеансе отправляется по сети без шифрования, эта информация доступна любому, кто прослушивает сетевой трафик.

В дополнение к указанному нами флагу HTTPOnly мы также можем установить флаг Secure в нашем заголовке Set-CookieHTTP. Это уведомит агента (т. Е. Браузер), что мы не хотим отправлять наши куки-файлы по любым HTTP-запросам, если это не безопасное соединение.

Заголовок HTTP:

Set-cookie: mycookie=value; path=/; HttpOnly secure

Опять же , мы можем легко сделать это, установив защищенное свойство на нашем объекте печенья «истина:»

app.use(session({
       //..previous removed for brevity

        cookie: {
            path: “/“,
            httpOnly: true,
            secure: true,
            maxAge:  1800000
        }
}));

Кроме того, распространенным сценарием, который помогает предотвратить использование защищенного флага, является риск смешанного содержимого. Довольно часто можно легко настроить страницу или сайт для использования безопасного HTTP (HTTPS), но отдельный ресурс на этой странице небезопасно запрашивается через HTTP.

Без флага Secure этот HTTP-запрос просочился бы в наш сеансовый файл cookie по этому небезопасному запросу для этих ресурсов.

Совет: Политики безопасности контента могут помочь в отчете, когда страница состоит из смешанного контента. В следующем посте мы рассмотрим, как политики безопасности контента (CSP) могут помочь в решении проблем со смешанным контентом.

К сожалению, с установленным флагом Secure, если мы не обслуживаем нашу страницу или сайт по HTTPS, мы исключили возможность отправлять наши сеансовые куки-файлы при каждом запросе, и это определенно не то, что мы хотим. Итак, в последнем разделе давайте рассмотрим важность использования безопасности транспортного уровня (TLS), когда речь идет о безопасном управлении нашими пользовательскими сеансами.

Т ransport Layer Security

Мы уже упоминали о риске атак «человек посередине» и о том, как любой, у кого есть средства, может легко прослушивать весь сетевой трафик. Однако, поскольку трафик небезопасен и не зашифрован, они также могут подслушивать информацию в этом трафике.

Небезопасный трафик для атак «человек посередине»

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

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

TLS предоставляет ряд преимуществ, таких как целостность передаваемой информации и достоверность сервера, с которым вы общаетесь. Кроме того, безопасность транспортного уровня обеспечивает конфиденциальность обмена информацией с помощью средств шифрования. Обслуживание контента вашего сайта по протоколу TLS может гарантировать, что ваш секретный файл cookie сеанса зашифрован и не будет виден любопытным взглядом атаки «человек посередине».

В следующем посте мы рассмотрим детали обслуживания нашего приложения Node.js по HTTPS. 

Заключение

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