Веб-разработчики! Пожалуйста, скажите «нет» LocalStorage


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


Главная проблема LocalStorage


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

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

Что такое локальное хранилище?



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

Начнём с основ. Локальное хранилище — это одна из новых возможностей HTML5, которая позволяет разработчику хранить любую необходимую ему информацию в браузере пользователя с применением средств JavaScript.

На практике локальное хранилище представляет собой обычный JS-объект, в который можно помещать данные или удалять их из него. Вот пример JS-кода, который сохраняет некие данные в локальном хранилище, потом получает их из него, и затем (эта часть закомментирована) удаляет их.

// Вот пара способов, которые можно использовать для записи данных в локальное хранилище.
localStorage.userName = "rdegges";
localStorage.setItem("favoriteColor", "black");

// После того, как данные окажутся в localStorage, они будут храниться там неограниченно
// долго, до тех пор, пока их не удалят явным образом.
alert(localStorage.userName + " really likes the color " + localStorage.favoriteColor + ".");

// Удаление данных из локального хранилища - операция довольно простая.
// Следующие строки, если их раскомментировать, приведут к удалению из
// хранилища записей userName и favoriteColor.
//localStorage.removeItem("userName");
//localStorage.removeItem("favoriteColor");

Если вы выполните этот код в браузере, поместив его в тестовую HTML-страницу, вы увидите фразу «rdegges really likes the color black», которая будет выведена в окне сообщения. Если затем вы откроете инструменты разработчика, то увидите, что переменные userName и favoriteColor сохранены в локальном хранилище браузера.


Данные в локальном хранилище браузера

Теперь, возможно, вы задаётесь вопросом о том, есть ли какие-то способы использования локального хранилища, при применении которых данные из хранилища в некий момент удаляются автоматически, что устранило бы необходимость в ручном удалении записей. К счастью, рабочая группа HTML5 (спасибо им!) это предусмотрела. Они позаботились о том, чтобы в HTML5 было реализовано так называемое сессионное хранилище (объект sessionStorage), которое работает точно так же, как локальное хранилище, за исключением того, что все данные из него удаляются после того, как пользователь закроет соответствующую вкладку браузера.

Сильные стороны локального хранилища



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

Главный плюс локального хранилища заключается в том, что работа с ним ведётся исключительно средствами JavaScript. Одно из раздражающих свойств куки (единственной реальной альтернативы локальному хранилищу) заключается в том, что они должны создаваться средствами веб-сервера. Вот так. Веб-серверы — это скучно, сложно, и с ними тяжело работать.

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

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

Ещё одно полезное свойство локального хранилища заключается в том, что его размер не так сильно ограничен, как размеры куки-файлов. Локальное хранилище даёт как минимум 5 Мб места под хранение данных во всех основных веб-браузерах, что несравнимо больше, чем 4 Кб (и это — максимум), которые можно сохранить средствами куки.

Это делает локальное хранилище особенно полезным, если нужно кэшировать данные какого-то приложения в браузере для того, чтобы воспользоваться ими позже. Так как 4 Кб, которые можно сохранить в куки, это совсем немного, локальное хранилище оказывается одной из наиболее жизнеспособных альтернатив механизму куки.

Слабые стороны локального хранилища



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

Локальное хранилище устроено предельно просто. Эх, когда я это сказал, мне прямо-таки полегчало. Локальное хранилище — это простейшее API.

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

  • Оно может хранить лишь строки. Да, именно так. Оно практически бесполезно для хранения данных, имеющих даже немного более сложную структуру, чем обычные строки. Конечно, всё что угодно можно сериализовать, преобразовать к строковому виду любые типы данных, поместить это в локальное хранилище, но такой ход выглядит не самым лучшим образом.
  • Взаимодействие с ним организовано синхронно. Это означает, что все операции с локальным хранилищем выполняются по одной, приложение ждёт их завершения для продолжения работы. Для сложных приложений это большой минус, так как это их замедляет.
  • Им не могут пользоваться веб-воркеры. Это означает, что если вы хотите создать приложение, которое задействует возможности фоновой обработки данных, стремясь к высокой производительности, или вы разрабатываете расширение для Chrome, или ещё что-то подобное, то вам не удастся использовать локальное хранилище, так как оно недоступно для веб-воркеров.
  • Объём данных, который помещается в локальное хранилище (около 5 Мб, как уже было сказано), не так уж и велик. На самом деле, это очень мало для приложений, интенсивно работающих с данными, или нуждающихся в возможности нормального функционирования без подключения к интернету.
  • Любой JS-код на странице может получить доступ к локальному хранилищу этой страницы. У локального хранилища нет абсолютно никаких механизмов защиты данных. Это недостаток можно признать основным в плане безопасности (и именно это меня больше всего беспокоит в последние годы).

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

  • Она общедоступна и не является секретной.
  • Её не планируется использовать в высокопроизводительных приложениях.
  • Её размер не превышает 5 Мб.
  • Вся она представлена исключительно строковыми данными.

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

Почему локальное хранилище небезопасно и им не следует пользоваться для хранения секретных данных



Вот в чём тут дело: большинство минусов локального хранилища не так уж и серьёзны. Его вполне можно использовать, это приведёт лишь к небольшому замедлению приложения и некоторым мелким, но вполне решаемым проблемам. Однако безопасность — это совсем другое дело. Модель безопасности локального хранилища — это по-настоящему важный вопрос, в котором нужно как следует разобраться, и который нужно понять, так как это может очень сильно повлиять на ваш сайт, привести к последствиям, о которых вы, вероятно, и не думали.

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

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

Знаете, что опаснее всего в нашем мире? Наверняка, вы ответили на этот вопрос правильно. Это — JavaScript.

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

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

Если атакующий может выполнять произвольный JS-код на вашем сайте, он может прочитать всё, что находится в локальном хранилище и отправить это куда угодно. Это означает, что всё ценное, находящееся в хранилище (вроде данных сессии) может быть скомпрометировано.
Тут вы можете подумать: «Ну и что? Мой сайт отлично защищён. Чужой JS-код на него не попадёт».

Это сильный аргумент. Если ваш сайт по-настоящему защищён, и никто не сможет запустить на нём свой JS-код, то, с технической точки зрения, данным в локальном хранилище ничто не угрожает. Однако в реальности достичь защиты такого уровня практически невозможно. У такого положения дел есть вполне простые и понятные причины. А именно, если ваш веб-проект содержит любой JS-код сторонних разработчиков, включённый в него из источников, находящихся за пределами вашего домена, то сайт оказывается в группе риска. У потенциального атакующего появляется возможность запустить на нём свой код. В частности, речь идёт о следующих возможных источниках небезопасного кода:

  • Ссылки на Bootstrap.
  • Ссылки на jQuery.
  • Ссылки на Vue, React, Angular и так далее.
  • Ссылки на код любых рекламных сетей.
  • Ссылки на Google Analytics.
  • Ссылки на любой код для отслеживания поведения пользователей.

Предположим, некая веб-страница содержит следующий тег <script>:

<script src="https://awesomejslibrary.com/minified.js"></script>

В данном случае, если awesomejslibrary.com взломают и внесут изменения в minified.js, благодаря которым смогут перебрать все данные, находящиеся в локальном хранилище и отправить их специальному API, созданному для сбора украденной информации, это означает, что от безопасности сайта, содержащего вышеприведённую строку кода, не останется и следа. В подобной ситуации атакующий сможет получить доступ ко всему, что было помещено в локальное хранилище, а владелец сайта даже об этом не узнает. На самом деле — это не самая приятная перспектива.

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

В большинстве компаний управлением веб-сайтом занимается отдел маркетинга, в котором используются различные WYSIWYG-редакторы и самые разные инструменты. Может ли в подобной ситуации кто-нибудь быть по-настоящему уверенным в том, что нигде на его сайте не используется JS-код сторонних разработчиков? Готов поспорить, что такой уверенности не может быть ни у кого.

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

Внимание: не храните данные JWT в локальном хранилище



Хотя мне кажется, что я предельно ясно разъяснил причины, по котором локальное хранилище просто нельзя использовать для хранения секретной информации, я чувствую, что нужно уделить особое внимание технологии JWT (JSON Web Tokens).

Пожалуй, самые серьёзные нарушители безопасности наших дней — это те из нас, кто хранит JWT (данные сессии) в локальном хранилище. Многие просто не понимают, что JWT — это то же самое, что комбинация имени пользователя и пароля.

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

Существует бесчисленное множество руководств, видеоуроков на YouTube, и даже курсов по программированию в университетах и в тренировочных центрах, где начинающих разработчиков учат хранить JWT в локальном хранилище в рамках механизма аутентификации. Это неправильно. Если вы попадёте на учебный курс, где вам об этом расскажут — бегите оттуда.

Альтернативы локальному хранилищу



Итак, если учесть все минусы локального хранилища, понятно, что вместо него нужно использовать что-то другое. Предлагаю рассмотреть альтернативы.

?Хранение секретных данных


Если вам нужно хранить секретные данные, всегда следует использовать серверную сессию. Вот что относится к секретным данным:

  • Идентификаторы пользователей.
  • Идентификаторы сессий.
  • JWT.
  • Персональная информация пользователей.
  • Данные кредитных карт.
  • Ключи API.

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

Если вам нужно хранить секретные данные — вот как это сделать:

  • Когда пользователь входит на веб-сайт, создайте идентификатор сессии для него и сохраните его в куки, подписав, используя методы криптографии. Если вы используете некий веб-фреймворк, поищите в его документации сведения о том, как создать пользовательскую сессию, используя куки, и следуйте найденному руководству.
  • Проверьте, чтобы в библиотеке для работе с куки, которую использует ваш фреймворк, была бы организована установка флага httpOnly для куки. Применение этого флага приводит к тому, что браузер не может прочесть любые куки, а это необходимо для того, чтобы безопасно использовать серверные сессии с куки. Вот полезный материал на эту тему.
  • Проверьте, чтобы ваша библиотека для работы с куки устанавливала бы флаг SameSite=strict (для предотвращения CSRF-атак), а также флаг secure=true (это обеспечивает возможность установки куки только через зашифрованное соединение).
  • Каждый раз, когда пользователь выполняет запрос к вашему сайту, используйте идентификатор его сессии (извлечённый из куки-файла, который он вам отправил) для получения сведений о его учётной записи либо из базы данных, либо из кэша (конкретный механизм зависит от размера веб-проекта).
  • После того, как сведения об учётной записи пользователя получены и проверены, можете запрашивать любые связанные с ними секретные данные.

Этот шаблон прост, понятен, и, что самое главное, он безопасен. Кроме того, что немаловажно, он хорошо масштабируется для применения в больших веб-проектах. И не рассказывайте мне о том, что механизм JWT не хранит сведения о состоянии, и о том, что он быстр, и вам необходимо использовать локальное хранилище для размещения в нём соответствующих токенов. Так поступать нельзя.

?Хранение данных, не являющихся строковыми


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

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

Вот хорошее руководство по IndexedDB.

?Хранение данных в оффлайне


Если вам нужно, чтобы приложение работало без подключения к интернету, лучше всего воспользоваться комбинацией из IndexedDB и Cache API (оно является частью сервис-воркеров).

Cache API позволяет кэшировать сетевые ресурсы, которые нужны приложению. Если вам нужны подробности о данном API, взгляните на это руководство.

Итоги


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

  • Данные не являются секретными.
  • Их не планируется использовать в приложениях, в которых делается упор на производительности.
  • Размер этих данных не превышает 5 Мб.
  • Они являются исключительно строковыми.

И пожалуйста, что бы вы ни делали, не храните данные сессий (вроде JSON Web Tokens) в локальном хранилище. Хранить там подобные данные — идея крайне неудачная, из-за этого ваш проект может быть подвержен чрезвычайно широкому спектру атак, способных серьёзно навредить и вам и вашим пользователям.

Кстати, если вы дочитали до этого места, то, возможно, задаётесь вопросом о том, почему выше, говоря о XSS-атаках, я не рассказал о том, что для борьбы с ними можно использовать политику защиты контента (CSP, Content Security Policy). Я намеренно об этом не упомянул, так как политики не помогают в описанной здесь ситуации. Даже если вы используете CSP для создания белых списков доменов, с которых загружается JS-код, это не поможет в том случае, если один из этих доменов будет взломан.

И, раз уж об этом зашла речь, скажу о том, что технология обеспечения целостности подресурсов (Subresource Integrity, SRI), хотя сама по себе и полезна, тоже не является исчерпывающим решением проблемы безопасности локального хранилища. Эта технология практически никогда не используется для маркетинговых инструментов или в рекламных сетях (а всё это, пожалуй, являет собой пример стороннего JS-кода, используемого чаще всего), так как их владельцы стремятся к тому, чтобы иметь возможность, без особых проблем, достаточно часто обновлять соответствующий код.

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

Уважаемые читатели! Пользуетесь ли вы локальным хранилищем в своих веб-проектах?




К сожалению, не доступен сервер mySQL