О себе
Всем привет! Меня зовут Мащенко Вадим, я работаю в группе разработки и тестирования и занимаюсь тестированием безопасности приложений Мир Plat.Form. С недавнего времени увлекся разработкой на фреймворке Angular. Я решил объединить свое новое увлечение со своей основной работой и показать результаты своего исследования в данной статье.В данной статье описано, как можно получить полезные данные Angular приложения, почему это важно и как от этого защититься.
Введение
Представьте себя в роли злоумышленника, который хочет взломать сайт. Как бы вы действовали? Атаку можно обобщить в два шага:- Изучение системы: стек технологий, поиск всех возможных точек входа, поиск известных уязвимостей в приложении и т.д.
- Эксплуатация найденных уязвимостей
Часто такой брешью в приложениях SPA (Single Page Application) является раскрытие информации о самом приложении и frontend и backend частей.
Дальнейшие примеры будут рассматриваться с использованием фреймворка Angular.
Немного про SPA
Существует два подхода в работе веб приложений:- Multi Page Application (многостраничное приложение) - сайт, который работает по традиционной схеме, когда на каждый запрос вам возвращается новый HTML файл. Т.е. при каждом запросе страница обновляется.
- Single Page Application (одностраничное приложение) - сайт, для работы которого не требуется обновление страницы, потому что весть код приложения загружается при инициализирующем запросе, а данные загружаются с помощью асинхронных запросов.
Немного про Angular
Простыми словами Angular - это SPA фреймворк, который построен на компонентах. Каждый angular компонент по сути отображает информацию и может быть частью другого компонента. Соответственно, все страницы приложения - это тоже компоненты.Также есть корневой компонент, внутрь которого вставляются другие компоненты. Это как аппликация, где основа - чистый лист бумаги, поверх которого вы клеите другие листы, на которых что-то нарисовано.
Раскрытие данных
Как говорилось ранее, главная особенность SPA - это то, что весь код приложения грузится на клиент при первом посещении сайта, что делает приложение отзывчивее, но при этом полностью открытым.Представьте, что вы заходите на сайт, где кроме страницы входа вам больше ничего не доступно, даже зарегистрироваться нет возможности. Мы можем открыть панель разработчика и посмотреть, что прилетает при первом запросе. Как правило, это следующие JS файлы приложения — бандлы:
- runtime.js - загрузчик Webpack, позволяющий загружать другие файлы;
- main.js - хранит код приложения;
- polyfills.js - позволяет обеспечить совместимость приложения с браузерами.
Например, у нас есть исходный код:
И есть этот же участок кода из бандла:
Сперва может показаться, что всё не совсем понятно, однако можно увидеть некоторое сходство.
Далее по сути реверс.
Например, атрибуты тега:
Сами теги:
Названия переменных:
Селектор компонента (его название), обычно связан с его логикой:
Таким образом, содержимое всех страниц сайта доступно пользователю с момента загрузки приложения. Это же касается и констант, переменных, например, endpoint-ов для получения данных и адреса серверов для запроса данных, что дает нам API-запросы.
Открываем панель разработчика, ищем и открываем main.js файл, далее через поиск ищем различные endpoint-ы.
Собираем все запросы и начинаем искать уязвимости на стороне backend-а. Тут можно сказать: "Это же проблемы backend, при чем здесь frontend?". Да, это проблемы backend-а, но упростил исследование приложения для неаутентифицированного пользователя frontend. Вместо перебора запросов у нас теперь есть определенные точки входа, некоторые из которых могут быть не защищены.
Тем самым, информация, которую мы можем получить с frontend части на Angular, может быть:
- конфигурация системы;
- API запросы;
- сервисы + компоненты;
- явно прописанные данные (ключи, логины, пароли и т.д.)
- ...
Как защититься?
В Angular есть интерфейсы для авторизации навигации, т.е. ограничения перехода пользователя по определенным роутам приложения. Классы, называемые Guard-ами, реализуют эти интерфейсы и определяют логику ограничений. Например, Guard может отправлять на сервер токен доступа пользователя и по ответу принимать решение, есть ли у пользователя доступ по данному роуту.Роут в Angular приложении - это привязка Angular компонента к URL, по которому его можно получить.
Обязательными атрибутами роута являются:
- path - относительный путь до компонента;
- component - привязанный к нему Angular компонент.
- CanActivate
- CanLoad
CanActivate
(https://angular.io/api/router/CanActivate)Еще один ключ объекта, canActivate, указывает на класс AuthGuard - реализация интерфейса CanActivate. В нём установлена проверка доступа к роутам.
В примере есть всего 2 роута: страница входа (login) и основная страница (main). При вводе пустого роута, будет происходить редирект на страницу "main". При каждом посещении данной страницы будет выполняться проверка доступа пользователя для данного роута через функцию canActivate.
Если у пользователя есть доступ, функция будет возвращать true, иначе функция вернет false.
Собираем такое приложение, запускаем и смотрим наши бандлы. По ключевым слова canActivate или auth находим нашу функцию canActivate и сервис по проверке доступа.
Почему так произошло? Angular - это модульное приложение, т.е. его можно разбивать на части (модули). Модуль - инкапсуляция функционала приложения. По умолчанию, в приложении Angular присутствует корневой модуль, в котором перечислены все компоненты, сервисы, Guard-ы и т.д.
Это значит, что механизм защиты, который мы писали для защиты кода попадает вместе с кодом клиенту при первом запросе на сервер.
Таким образом такая защита роутов не является надежной.
Can Load
(https://angular.io/api/router/CanLoad)Lazy Load (Ленивая загрузка) позволяет подгружать некоторые бандлы отдельно от основного приложения.
Мы можем разделить наше приложение, создав новые модули приложения, которые будут загружаться отдельными бандлами при обращении к данному роуту.
В нашем случае мы создаем новый модуль, за которым будут закреплены свои компоненты (например, компонент "Список постов"), и связываем его с новым роутом.
Таким образом, пока мы не обратимся по пути /posts, данный модуль не будет загружен. Однако, сделав это будучи не авторизованным пользователем, мы также сможем подгрузить данный участок кода.
Для таких случаев используется Guard CanLoad (https://angular.io/api/router/CanLoad), который, как и CanActivate, записывается в качестве атрибута роута и ссылается на файл, где выполняется проверка доступа пользователя для данного роута.
Таким образом, если мы не залогинились на сайте, обращаясь по пути /users, ничего загружаться не будет.
Однако это ненадежная защита, т.к. обход такой защиты есть.
Как говорилось ранее, Angular - это модульное приложение, и, даже если мы используем ленивую загрузку модулей, упоминание о них есть в корневом модуле. Таким образом задача сводится к тому, чтобы найти имя бандла модуля. Сделать это можно, если искать в runtime.js файле.
Зная имя файла мы сможем открыть код Lazy Load модуля в браузере. Просто берем атрибут хэша, сам хэш, добавляем в конце ".js" и подставляем в хосту: http://{host}/275.f668…324.js
Таким образом Guard-ы не скроют должным образом необходимую информацию.
CTF
В рамках проведения локальных соревнований по компьютерной безопасности (Capture The Flag) я подготовил задание, связанное с Angular Lazy Load (https://gitlab.com/v4dimm/lazy-load). Необходимо найти флаг (секретное слово), заполучив права администратора, имея только пользователя без соответствующих прав.Запустите приложение и попробуйте достать флаг методом black box.
Какие у нас есть варианты защиты?
Можно предложить 2 варианта:
- Сделать код нечитаемым для злоумышленника
- Хранить часть кода на другом сервере (по сути, использование микрофрентенда)
Нечитаемый код
Обфускация (процесс создания нечитаемого кода) JS кода - это ненадежная защита из-за особенностей реализации JS. Например, при обфускации сохраняется структура кода, т.к. обфускатор не понимает, как он будет исполняться. Тем самым задача обфускации JS кода сводится к его минимизации.Деобфускация JS кода - это лишь вопрос времени. Достаточно понять алгоритм обфусцирования, и его можно автоматизировать и применить к остальным файлам. К тому же, если разработчики оставили source map - механизм для связи минифицированного файла с исходным, то в обфускации с точки зрения безопасности вообще нет смысла. Тут можете погуглить различные исследования на эту тему. Есть еще вариант попробовать использовать сборщик gcc (https://www.npmjs.com/package/google-closure-compiler) для изменения структуры самого бандла, но это уже не про нечитаемый код и не тема данной статьи.
Микрофронтенд
Рассмотрим несколько вариантов скрытия информации SPA приложений. В рамках приведенных примеров будем считать backend априори безопасным. Все проверки доступа пользователей происходят на стороне backend.Server Side Rendering
Обычное приложение Angular выполняется в браузере, отображая страницы в DOM в ответ на действия пользователя. С использованием Angular Universal (опенсорсный проект, который расширяет функциональность Angular приложений, делая возможным SSR в Angular) приложение запускается на сервере, генерируя статические страницы приложения, которые впоследствии загружаются на клиент.Обычно это делается для оптимизации и для поисковиков, чтобы была информация для индексации.
При использовании Angular Universal создаются новые файлы для работы на стороне сервера. Нас интересует файл сервера - server.ts.
Это Express сервер (он используется по умолчанию) и в нем можно прописать дополнительные обработки маршрутов.
Возьмем приложение, которое использует Lazy Load. При сборке все файлы будут храниться на сервере, включая бандлы, которые не должны быть на клиенте при инициализирующем запросе. Сделаем на сервере обработку прямых обращений к этим бандлам (для начала попробуем возвращать 403 ошибку).
При попытке загрузить бандл, вернётся 403 ошибка.
Мы смогли защитить часть приложения от раскрытия данных. Далее можем писать в тело функции собственные проверки.
Особенности данного подхода:
- Определенный backend (Express, ASP.NET Core, hapi). Если в рамках разработки уже есть свой бэк, например на Java, то Express можно использовать как proxy.
- Придётся поправлять код под серверную часть, иначе могут возникнуть ошибки, например, работа с localStorage, т.к. при создании файлов сервера код копируется из браузерной части. Если была работа с localStorage, на стороне бэка будет ошибка, т.к. там нет localStorage.
Angular Elements
Есть такая технология как Web Components, которая позволяет вставлять в приложение некоторый функционал посредством создания пользовательских HTML-элементов. Это позволяет делать Web Components универсальными для разных фреймворков.Angular Elements - это по сути Custom Elements (пользовательские элементы - часть Web Component), возможность создавать новые HTML-теги с произвольным имене из Angular приложения.
Рассмотрим на примере. Создадим приложение, которое хотим использовать как Angular Element. Убираем загрузку корневого компонентам при запуске приложения (bootstrap) в корневом модуле. Добавляем его загрузку через injector - часть механизма Dependency Injection. Там же помечаем название нового тега.
В момент, когда на основном приложении выполняется необходимое условие, например, пользователь успешно прошел аутентификацию, созданный нами HTML-тег и скрипты (основные бандлы для работы Angular Element) можно просто вставить в HTML файл.
Таким образом на страничке появится новый функционал и отображение, которых ранее не было в приложении.
Особенности данного подхода:
- Сложная реализация и поддержка такого решения.
- Routing внутри Angular Elements работать не будет. Придется искать обходные пути, либо на каждый отдельный роут использовать отдельный Angular Element.
Lazy Load с сервера
Вспомним про ленивую загрузку в Angular.Если посмотреть, как билдится (собирается) приложение, можно увидеть помимо основных бандлов создание lazy chunk файлов, которые отвечают за подгружаемые модули.
Упоминания о которых также есть в runtime.js файле
Посмотрим на этот файл в дебаг сборке командой:
$ NG_BUILD_DEBUG_OPTIMIZE=true ng build
Можно увидеть, что это chunkId, которые используются webpack-ом (сборщиком модулей JS):
Идем дальше и видим, что есть переменная url, которая формируется с помощью chunkId и еще какой-то переменной:
Если вернуться к сжатому представлению файла runtime.js файла, то данная строка будет иметь данный формат:
Там же можно заметить переменную, которая также участвует в формировании url-а.
И если явно записать в эту переменную адрес сервера, где будут лежать lazy chunk файлы:
То при запуске приложения можно увидеть такую картину
Когда мы переходим по роуту, за которым закреплен другой модуль, т.е. когда подтягивается lazy chunk файл, запрос идёт на другой сервер. Т.к. бандл больше не хранится на фронте, а лежит на backend-а, где выполняется проверка для его получения, то можно считать, что нам удалось скрыть часть данных приложения.
Особенности данного подхода:
- Проблематично автоматизировать такое решение
- На backend можно будет ходить только с cookie. Запрос на загрузку JS файлов frontend делает автоматически без возможности вставить туда дополнительные HTTP заголовки. Единственный заголовок, которые появляется автоматически - это Cookie, если для данного сайта есть файлы cookie.
Заключение
Мы рассмотрели такую проблему безопасности, как раскрытие данных в SPA приложениях, в частности фреймворка Angular. Воспользоватлись стандартными средствами защиты и обошли их. Далее разделили наше приложение на отдельные модули, которые необходимо загружить отдельно от основного приложение с помощью следующих техник:- Server Side Rendering
- Angular Elements
- Lazy Load с севера
На этом всё, буду рад, если мой опыт и исследование окажется полезными. Программируйте безопасно!
Особенности Angular с точки зрения безопасности
О себе Всем привет! Меня зовут Вадим, я работаю в группе разработки и тестирования и занимаюсь тестированием безопасности приложений Мир Plat.Form. С недавнего времени увлекся разработкой на...
habr.com