- PVSM.RU - https://www.pvsm.ru -
Однажды я отправился на поиск учебных руководств по аутентификации в Node.js/Express.js, но, к сожалению, не смог найти ни одного, которое меня бы полностью устроило. Некоторые были неполными, некоторые содержали ошибки в сфере безопасности, вполне способные навредить неопытным разработчикам.
Сразу скажу, что я всё ещё нахожусь в поиске надёжного, всеобъемлющего решения для аутентификации в Node/Express, которое способно составить конкуренцию Devise [1] для Rails. Однако, удручающая ситуация в сфере руководств подвигла меня на подготовку этого материала. Тут я разберу некоторые наиболее распространённые ошибки в области аутентификации и расскажу о том, как их избежать.
Выше я говорил о «неопытных разработчиках». Кто они? Например — это тысячи фронтенд-программистов, брошенных в водоворот серверного JS, которые пытаются набраться практического опыта из руководств, либо просто копипастят всё, что попадётся под руку и устанавливают всё подряд с помощью npm install. Почему бы им не вложить время в серьёзное изучение вопроса? Дело в том, что они копипастят не от хорошей жизни, им приходится из кожи вон лезть, чтобы уложиться в сроки, установленные аутсорс-менеджерами, или кем-то вроде креативных директоров рекламных агентств.
На самом деле, пока я искал подходящее руководство по аутентификации, я проникся ощущением, что каждый Node-программист, у которого есть блог, опубликовал собственный тьюториал, посвящённый тому, как «делать это правильно», или, точнее, о том, «как это делается».
Ещё одна неоднозначная вещь в разработке для Node.js заключается в отсутствии некоего всеобъемлющего, надёжного решения для аутентификации. Этот вопрос, в основном, рассматривается в качестве чего-то вроде упражнения для программиста. Стандартом де-факто для Express.js является Passport [2], однако, это решение предлагает лишь набор стратегий аутентификации. Если вам нужно надёжное решение для Node, вроде Platformatec Devise [1] для Ruby on Rails, вам, вероятно, придётся обратиться к Auth0 [3] — стартапу, который предлагает аутентификацию как сервис.
Passport, в отличие от полномасштабного Devise, представляет собой промежуточный программный слой, который, сам по себе, не охватывает все части процесса аутентификации. Используя Passport, Node-разработчику придётся создать собственное API для механизма токенов и для сброса пароля. Ему придётся подготовить маршруты и конечные точки аутентификации пользователей. На нём же лежит и создание интерфейсов с использованием, например, некоего популярного языка шаблонов. Именно поэтому существует множество учебных руководств, которые направлены на помощь в установке Passport для Express.js-приложений. Практически все они содержат те или иные ошибки. Ни одно из них не позволяет создать полномасштабное решение, необходимое для работающего веб-приложения.
Хотелось бы отметить, что я не собираюсь нападать на конкретных создателей этих руководств, скорее я использую их ошибки для того, чтобы продемонстрировать проблемы с безопасностью, связанные с развёртыванием ваших собственных систем аутентификации. Если вы — автор подобного руководства — дайте мне знать [4], если после чтения этого материала внесёте в своё руководство правки. Сделаем экосистему Node/Express безопаснее и доступнее для новых разработчиков.
Начнём с хранилища учётных данных. Запись и чтение учётных данных — это вполне обычные задачи в сфере управления аутентификацией, и традиционный способ решения этих задач заключается в использовании собственной базы данных. Passport является промежуточным программным обеспечением, которое просто сообщает нашему приложению: «этот пользователь прошёл проверку», или: «этот пользователь проверку не прошёл», требуя модуля passport-local [5] для работы с хранилищем паролей в локальной базе данных. Этот модуль написан тем же разработчиком, что и сам Passport.js.
Прежде чем мы спустимся в эту кроличью нору учебных руководств, вспомним об отличной шпаргалке по хранению паролей [6], подготовленной OWASP, которая сводится к тому, что нужно хранить высокоэнтропийные пароли с уникальной «солью» и c применением односторонних адаптивных функций хэширования. Тут можно вспомнить и bcrypt-мем [7] с codahale.com, даже несмотря на то, что по данному вопросу имеются некоторые разногласия [8].
Я, в поиске того, что мне нужно, повторяя путь нового пользователя Express.js и Passport, сначала заглянул в примеры к самому passport-local. Там оказался шаблон приложения Express 4.0., который я мог скопировать и расширить под свои нужды. Однако, после простого копирования этого кода, я получал не так уж и много полезностей. Например, здесь не оказалось подсистемы поддержки базы данных. Пример подразумевал простое использование некоторого набора аккаунтов.
На первый взгляд всё вроде бы нормально. Перед нами — обычное интранет-приложение. Разработчик, который воспользуется им, загружен донельзя, у него нет времени что-то серьёзно улучшать. Неважно, что пароли в примере не хэшируются. Они хранятся в виде обычного текста [9] прямо рядом с кодом логики валидации. А хранилище учётных данных здесь даже не рассматривается. Чудная получится система аутентификации.
Поищем ещё одно учебное руководство по passport-local. Например, мне попался этот [10] материал от RisingStack, который входит в серию руководств «Node Hero». Однако, и эта публикация мне совершенно не помогла. Она тоже давала пример приложения [11] на GitHub, но имела те же проблемы, что и официальное руководство. Тут, однако, надо отметить, что 8-го августа стало известно о том, что RisingStack теперь использует bcrypt [12] в своём демонстрационном приложении.
Далее, вот ещё один результат из Google, выданный по запросу express js passport-local tutorial
. Руководство написано в 2015-м. Оно использует Mongoose ODM и читает учётные данные из базы данных. Тут есть всё, включая интеграционные тесты, и, конечно, ещё один шаблон, который можно использовать. Однако, Mongoose ODM хранит пароли, используя тип данных String [13], как и в предыдущих руководствах, в виде обычного текста, только на этот раз в экземпляре MongoDB. А всем известно, что экземпляры MongoDB обычно очень хорошо защищены [14].
Вы можете обвинить меня в пристрастном выборе учебных руководств, и вы будете правы, если пристрастный выбор означает щелчок по ссылке с первой страницы выдачи Google.
Возьмём теперь материал с самого верха страницы результатов поиска — руководство по passport-local [15] от TutsPlus. Это руководство лучше, тут используют [16] bcrypt с коэффициентом трудоёмкости 10 для хэширования паролей и замедляют синхронные проверки хэша, используя process.nextTick
.
Самый верхний результат в Google, это руководство от scotch.io [17], в котором так же используется bcrypt с меньшим коэффициентом трудоёмкости, равным 8. И 8, и 10 — это мало, но 8 — это очень мало. Большинство современных bcrypt-библиотек используют 12. Коэффициент трудоёмкости 8 был хорош для административных учётных записей восемнадцать лет назад [18], сразу после выпуска первой спецификации bcrypt.
Если даже не говорить о хранении секретных данных, ни одно из этих руководств не показывало реализацию механизма сброса паролей. Эта важнейшая часть системы аутентификации, в которой немало подводных камней, была оставлена в качестве упражнения для разработчика.
Схожая проблема в области безопасности — сброс пароля. Ни одно из руководств, находящихся в верхней части поисковой выдачи, совершенно ничего не говорит о том, как делать это с использованием Passport. Чтобы это узнать, придётся искать что-то другое.
В деле сброса пароля есть тысячи способов всё испортить. Вот наиболее распространённые ошибки в решении этой задачи, которые мне довелось видеть:
Если вы со всем этим никогда не сталкивались, взгляните на шпаргалку [20] по сбросу паролей, подготовленную OWASP. Теперь, обсудив общие вопросы, перейдём к конкретике, посмотрим, что может предложить экосистема Node.
Ненадолго обратимся к npm и посмотрим [21], сделал ли кто-нибудь библиотеку для сброса паролей. Вот, например, пакет [22] пятилетней давности от в целом замечательного издателя substack. Учитывая скорость развития Node, этот пакет напоминает динозавра, и если бы мне хотелось попридираться к мелочам, то я мог бы сказать, что функция Math.random()
предсказуема [23] в V8, поэтому её не следует использовать [23] для создания токенов. Кроме того, этот пакет не использует Passport, поэтому мы идём дальше.
Stack Overflow здесь тоже особенно не помог. Как оказалось, разработчики из компании Stormpath любят писать о своём IaaS-стартапе в любом посте, хоть как-то связанным с этой темой. Их документация [24] тоже всплывает повсюду, они также продвигают свой блог, где есть материалы [25] по сбросу паролей. Однако, чтение всего этого — пустая трата времени. Stormpath — проект нерабочий, 17 августа 2017-го он закрывается [26].
Ладно, возвращаемся к поиску в Google. На самом деле, такое ощущение, что интересующая нас тема раскрыта в единственном материале. Возьмём первый результат [27], найденный по запросу express passport password reset
. Тут снова встречаем нашего старого друга bcrypt, с даже меньшим коэффициентом трудоёмкости, равным 5, что значительно меньше, чем нужно в современных условиях.
Однако, это руководство выглядит довольно-таки целостным по сравнению с другими, так как оно использует crypto.randomBytes
для создания по-настоящему случайных токенов, срок действия которых истекает, если они не были использованы. Однако, пункты 2 и 4 из вышеприведённого списка ошибок при сбросе пароля в этом серьёзном руководстве не учтены. Токены хранятся ненадёжно — вспоминаем первую ошибку руководств по аутентификации, связанную с хранением учётных данных.
Хорошо хотя бы то, что украденные из такой системы токены имеют ограниченный срок действия. Однако, работать с этими токенами очень весело, если у атакующего есть доступ к объектам пользователей в базе данных через BSON-инъекцию, или есть свободный доступ к Mongo из-за неправильной настройки СУБД. Атакующий может просто запустить процесс сброса пароля для каждого пользователя, прочитать незашифрованные токены из базы данных и создать собственные пароли для учётных записей пользователей, вместо того, чтобы заниматься ресурсоёмкой атакой по словарю на хэши bcrypt с использованием мощного компьютера с несколькими видеокартами.
Токены API — это тоже учётные данные. Они так же важны, как пароли и токены сброса паролей. Практически все разработчики знают это и стараются очень надёжно хранить свои ключи AWS, коды доступа к Twitter и другие подобные вещи, однако, к программам, которые они пишут, это часто не относится.
Воспользуемся системой JSON Web Tokens [28] (JWT) для создания учётных данных доступа к API. Применение токенов без состояния, которые можно добавлять в чёрные списки и нужно запрашивать, это лучше, чем старый шаблон API key/secret, который использовался в последние годы. Возможно, наш начинающий Node.js-разработчик где-то слышал о JWT, или даже видел пакет passport-jwt и решил реализовать в своём проекте стратегию JWT. В любом случае, JWT — это то место, где кажется, что все попадают в сферу влияния Node.js. (Почтенный Томас Пташек заявит [29], что JWT — это плохо, но я сомневаюсь, что его кто-нибудь услышит).
Поищем по словам express js jwt
в Google и откроем первый материал в поисковой выдаче, руководство [30] Сони Панди [31] об аутентификации пользователей с применением JWT. К несчастью, этот материал нам ничем не поможет, так как в нём не используется Passport, но пока мы на него смотрим, отметим некоторые ошибки в хранении учётных данных:
Да уж… Вернёмся к Google и поищем ещё руководств. Ресурс scotch.io, который, в руководстве по passport-local, проделал замечательную работу, касающуюся хранилища паролей, просто игнорирует [35] свои же идеи и хранит пароли в новом примере в виде обычного текста.
Однако, я решил дать этому руководству шанс, хотя его нельзя рекомендовать любителям копипастить. Это из-за одной интересной особенности, которая заключается в том, что тут выполняется сериализация объекта пользователя [36] Mongoose в JWT.
Клонируем репозиторий этого руководства, следуя инструкциям развернём и запустим приложение. После нескольких DeprecationWarning
от Mongoose можно будет перейти на http://localhost:8080/setup
и создать пользователя. Затем, отправив на /api/authenticate
учётные данные — «Nick Cerminara» и «password», мы получим токен, Просмотрим его в Postman.
JWT-токен, полученный из программы, описанной в руководстве scotch.io
Обратите внимание на то, что JWT-токен подписан, но не зашифрован. Это означает, что большой фрагмент двоичных данных между двумя точками — это объект в кодировке Base64. По-быстрому его раскодируем и перед нами откроется кое-что интересное.
Что может быть лучше пароля в виде обычного текста
Теперь любой, у кого есть токен, даже такой, срок действия которого истёк, имеет пароль пользователя, а заодно и всё остальное, что хранится в модели Mongoose. Учитывая то, что токен передаётся по HTTP, завладеть им можно с помощью обычного сниффера.
Как насчёт ещё одного руководства? Оно [37] рассчитано на новичков и посвящено аутентификации с использованием Express, Passport и JWT. В нём наблюдается та же уязвимость, связанная с раскрытием информации. Следующее руководство [38], подготовленное стартапом SlatePeak, выполняет такую же сериализацию. На данном этапе я прекратил поиски.
Я не нашёл упоминаний об ограничении числа попыток аутентификации или о блокировке аккаунта ни в одном из рассмотренных руководств.
Без ограничения числа попыток аутентификации, атакующий может выполнить онлайновую атаку по словарю, например, с помощью Burp Intruder [39], в надежде получить доступ к учётной записи со слабым паролем. Блокировка аккаунта так же помогает в решении этой проблемы благодаря требованию ввода дополнительной информации при следующей попытке входа в систему.
Помните о том, что ограничение числа попыток аутентификации способствует доступности сервиса. Так, использование bcrypt создаёт серьёзную нагрузку на процессор. Без ограничений, функции, в которых вызывают bcrypt, становятся вектором отказа в обслуживании уровня приложения, особенно при использовании высоких значений коэффициента трудоёмкости. В результате обработка множества запросов на регистрацию пользователей или на вход в систему с проверкой пароля создаёт высокую нагрузку на сервер.
Хотя подходящего учебного руководства на эту тему у меня нет, есть множество вспомогательных библиотек для ограничения числа запросов, таких, как express-rate-limit [40], express-limiter [41], и express-brute [42]. Не могу говорить об уровне безопасности этих модулей, я их даже не изучал. В целом, я порекомендовал бы использовать в рабочих системах обратный прокси [43] и передавать обработку ограничения числа запросов nginx [44] или любому другому балансировщику нагрузки.
Скорее всего авторы учебных руководств будут защищать себя со словами: «Это лишь объяснение основ! Уверены, никто не будет использовать этого в продакшне!». Однако, я не могу не указать на то, что эти слова не соответствуют действительности. Это особенно справедливо, если к учебным руководствам прилагается код. Люди верят словам авторов руководств, у которых гораздо больше опыта, чем у тех, кто руководства читает.
Если вы — начинающий разработчик — не доверяйте учебным руководствам. Копипастинг кода из таких материалов, наверняка, приведёт вас, вашу компанию, и ваших клиентов, к проблемам в сфере Node.js-аутентификации. Если вам действительно нужны надёжные, готовые к использованию в продакшне, всеобъемлющие библиотеки для аутентификации, взгляните на что-то, чем вам удобно будет пользоваться, на что-то, что обладает большей стабильностью и лучше испытано временем. Например — на связку Rails/Devise.
Экосистема Node.js, несмотря на свою доступность, всё ещё таит множество опасностей для JS-разработчиков, которым нужно срочно написать веб-приложение для решения реальных задач. Если ваш опыт ограничивается фронтендом, и ничего кроме JavaScript вы не знаете, лично я уверен в том, что легче взять Ruby и встать на плечи гигантов, вместо того, чтобы быстро научиться тому, как не отстрелить себе ногу, программируя подобные решения с нуля для Node.
Если вы — автор учебного руководства, пожалуйста, обновите его, в особенности это касается шаблонного кода. Этот код попадёт в продакшн.
Если вы — убеждённый Node.js-разработчик, надеюсь, вы нашли в моём рассказе что-нибудь полезное, касающееся того, чего лучше не делать в вашей системе аутентификации, основанной на Passport. Наверняка, если такая система у вас уже есть, что-то в ней сделано неправильно. Я не говорю о том, что мой материал покрывает все возможные ошибки аутентификации. Создание системы аутентификации для Express-приложения — это задача разработчика, который понимает все тонкости конкретного проекта. В результате получиться у него должно что-то качественное и надёжное. Если вы хотите обсудить вопросы защиты веб-приложений на Node.js — отправьте мне сообщение [4] в Twitter.
Автор публикации сообщает, что 7-го августа, с ним связались представители RisingStack. Они сообщили о том, что в их учебных руководствах пароли больше не хранятся в виде обычного текста [12]. Теперь в коде и руководствах они используют bcrypt.
Кроме того, он, вдохновлённый откликами на свой материал, создал этот документ [45], в котором намеревается собрать всё лучшее из области аутентификации в Node.js.
Уважаемые читатели! Что вы можете сказать об организации системы аутентификации в веб-приложениях, основанных на Node.js?
Автор: RUVDS.com
Источник [46]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/262076
Ссылки в тексте:
[1] Devise: https://github.com/plataformatec/devise
[2] Passport: http://passportjs.org/
[3] Auth0: https://auth0.com/
[4] дайте мне знать: https://twitter.com/_micaksica
[5] passport-local: https://github.com/jaredhanson/passport-local
[6] шпаргалке по хранению паролей: https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet
[7] bcrypt-мем: https://codahale.com/how-to-safely-store-a-password/
[8] разногласия: https://security.stackexchange.com/a/6415
[9] обычного текста: https://github.com/passport/express-4.x-local-example/blob/master/db/users.js
[10] этот: https://blog.risingstack.com/node-hero-node-js-authentication-passport-js/
[11] пример приложения: https://github.com/RisingStack/nodehero-authentication
[12] теперь использует bcrypt: https://github.com/RisingStack/nodehero-authentication/commit/9d69ea70b68c4971466c64382e5f038e3eda8d8a
[13] тип данных String: https://github.com/mjhea0/passport-local-express4/blob/master/models/account.js#L7
[14] очень хорошо защищены: https://www.shodan.io/report/nlrw9g59
[15] руководство по passport-local: https://code.tutsplus.com/tutorials/authenticating-nodejs-applications-with-passport--cms-21619
[16] используют: https://github.com/tutsplus/passport-mongo/blob/master/passport/login.js
[17] руководство от scotch.io: https://scotch.io/tutorials/easy-node-authentication-setup-and-local
[18] восемнадцать лет назад: https://www.usenix.org/legacy/publications/library/proceedings/usenix99/provos/provos_html/node6.html
[19] проблемы: https://www.kaspersky.com/blog/security-questions-are-insecure/13004/
[20] шпаргалку: https://www.owasp.org/index.php/Forgot_Password_Cheat_Sheet
[21] посмотрим: https://www.npmjs.com/search?q=password%20reset&page=1&ranking=popularity
[22] пакет: https://www.npmjs.com/package/password-reset
[23] предсказуема: https://github.com/substack/node-password-reset/blob/master/index.js#L73
[24] документация: https://docs.stormpath.com/client-api/product-guide/latest/password_reset.html
[25] материалы: https://stormpath.com/blog/the-pain-of-password-reset
[26] закрывается: https://stormpath.com/
[27] первый результат: http://sahatyalkabov.com/how-to-implement-password-reset-in-nodejs/
[28] JSON Web Tokens: https://jwt.io/
[29] заявит: https://news.ycombinator.com/item?id=13866883
[30] руководство: https://medium.com/@pandeysoni/user-authentication-using-jwt-json-web-token-in-node-js-using-express-framework-543151a38ea1
[31] Сони Панди: https://medium.com/@pandeysoni
[32] хранятся в виде обычного текста: https://github.com/pandeysoni/User-Authentication-using-JWT-JSON-Web-Token-in-Node.js-Express/blob/master/server/config/config.js#L13
[33] симметричный шифр: https://github.com/pandeysoni/User-Authentication-using-JWT-JSON-Web-Token-in-Node.js-Express/blob/master/server/config/common.js#L54
[34] уязвимыми: https://crypto.stackexchange.com/a/33861
[35] игнорирует: https://github.com/scotch-io/node-token-authentication/blob/master/app/models/user.js#L7
[36] сериализация объекта пользователя: https://github.com/scotch-io/node-token-authentication/blob/master/server.js#L81
[37] Оно: https://jonathanmh.com/express-passport-json-web-token-jwt-authentication-beginners/
[38] Следующее руководство: http://blog.slatepeak.com/creating-a-simple-node-express-api-authentication-system-with-passport-and-jwt/
[39] Burp Intruder: https://portswigger.net/burp/help/intruder_using.html
[40] express-rate-limit: https://github.com/nfriedly/express-rate-limit
[41] express-limiter: https://www.npmjs.com/package/express-limiter
[42] express-brute: https://github.com/AdamPflug/express-brute
[43] обратный прокси: https://expressjs.com/en/advanced/best-practice-performance.html#use-a-reverse-proxy
[44] nginx: https://www.nginx.com/blog/rate-limiting-nginx/
[45] этот документ: https://gitlab.com/micaksica/nodeauth-best-practices/tree/master
[46] Источник: https://habrahabr.ru/post/335434/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.