О себе

Всем привет! Меня зовут Мащенко Вадим, я работаю в группе разработки и тестирования и занимаюсь тестированием безопасности приложений Мир Plat.Form. С недавнего времени увлекся разработкой на фреймворке Angular. Я решил объединить свое новое увлечение со своей основной работой и показать результаты своего исследования в данной статье.

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

Введение

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

  1. Изучение системы: стек технологий, поиск всех возможных точек входа, поиск известных уязвимостей в приложении и т.д.

  2. Эксплуатация найденных уязвимостей

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

Часто такой брешью в приложениях SPA (Single Page Application) является раскрытие информации о самом приложении и frontend и backend частей.

Дальнейшие примеры будут рассматриваться с использованием фреймворка Angular.

Немного про SPA

Существует два подхода в работе веб приложений:

  1. Multi Page Application (многостраничное приложение) - сайт, который работает по традиционной схеме, когда на каждый запрос вам возвращается новый HTML файл. Т.е. при каждом запросе страница обновляется.

  2. Single Page Application (одностраничное приложение) - сайт, для работы которого не требуется обновление страницы, потому что весть код приложения загружается при инициализирующем запросе, а данные загружаются с помощью асинхронных запросов. 

Немного про Angular

Простыми словами Angular - это SPA фреймворк, который построен на компонентах. Каждый angular компонент по сути отображает информацию и может быть частью другого компонента. Соответственно, все страницы приложения - это тоже компоненты. 

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

Раскрытие данных

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

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

  • runtime.js - загрузчик Webpack, позволяющий загружать другие файлы;

  • main.js - хранит код приложения;

  • polyfills.js - позволяет обеспечить совместимость приложения с браузерами.

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

Например, у нас есть исходный код:

И есть этот же участок кода из бандла:

Сперва может показаться, что всё не совсем понятно, однако можно увидеть некоторое сходство.

Далее по сути реверс.

Например, атрибуты тега:

Сами теги:

Названия переменных:

Селектор компонента (его название), обычно связан с его логикой:

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

Открываем панель разработчика, ищем и открываем main.js файл, далее через поиск ищем различные endpoint-ы. 

Найденные API запросы системы. Часть адреса скрыта.
Найденные API запросы системы. Часть адреса скрыта.

Собираем все запросы и начинаем искать уязвимости на стороне backend-а. Тут можно сказать: "Это же проблемы backend, при чем здесь frontend?". Да, это проблемы backend-а, но упростил исследование приложения для неаутентифицированного пользователя frontend. Вместо перебора запросов у нас теперь есть определенные точки входа, некоторые из которых могут быть не защищены.

Тем самым, информация, которую мы можем получить с frontend части на Angular, может быть:

  • конфигурация системы;

  • API запросы;

  • сервисы + компоненты;

  • явно прописанные данные (ключи, логины, пароли и т.д.)

  • ...

Как защититься?

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

Роут в Angular приложении - это привязка Angular компонента к URL, по которому его можно получить. 

Два роута приложения - страница входа и основная страница
Два роута приложения - страница входа и основная страница

Обязательными атрибутами роута являются: 

  1. path - относительный путь до компонента;

  2. component - привязанный к нему Angular компонент.

Также могут быть необязательные атрибуты, такие как Guard-ы:

  1. CanActivate

  2. CanLoad

CanActivate

(https://angular.io/api/router/CanActivate

Еще один ключ объекта, canActivate, указывает на класс AuthGuard - реализация интерфейса CanActivate. В нём установлена проверка доступа к роутам.

Все роуты, кроме /login, недоступны неаутентифицированным пользователям.
Все роуты, кроме /login, недоступны неаутентифицированным пользователям.

В примере есть всего 2 роута: страница входа (login) и основная страница (main). При вводе пустого роута, будет происходить редирект на страницу "main". При каждом посещении данной страницы будет выполняться проверка доступа пользователя для данного роута через функцию canActivate.

Если у пользователя есть доступ, функция будет возвращать true, иначе функция вернет false.

Собираем такое приложение, запускаем и смотрим наши бандлы. По ключевым слова canActivate или auth находим нашу функцию canActivate и сервис по проверке доступа.

Найденный canActivate в бандле приложения
Найденный 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 модулей
Хэши двух Lazy Load модулей

Зная имя файла мы сможем открыть код 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 варианта:

  1. Сделать код нечитаемым для злоумышленника

  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 в SSR
Запуск стартового приложения Angular в SSR

При использовании Angular Universal создаются новые файлы для работы на стороне сервера. Нас интересует файл сервера - server.ts.

Созданные файлы сервера
Созданные файлы сервера

Это Express сервер (он используется по умолчанию) и в нем можно прописать дополнительные обработки маршрутов. 

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

При обращении к бандлам возвращается 403 ошибка - нет доступа
При обращении к бандлам возвращается 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 файл.

Таким образом на страничке появится новый функционал и отображение, которых ранее не было в приложении.

Angular Element и его бандлы
Angular Element и его бандлы

Особенности данного подхода:

  • Сложная реализация и поддержка такого решения.

  • Routing внутри Angular Elements работать не будет. Придется искать обходные пути, либо на каждый отдельный роут использовать отдельный Angular Element.

Есть проект, в котором можно посмотреть реализацию загрузки модулей (по сути Lazy Load) через Angular Elements: https://github.com/konstantindenerz/angular-lazy-loading-modules-different-server.

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 и в целом SPA приложений. 

На этом всё, буду рад, если мой опыт и исследование окажется полезными. Программируйте безопасно!

Комментарии (6)


  1. AlexSpaizNet
    22.12.2021 11:37

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

    А если зарегаться может любой то смысла большого нет. Всегда можно зарегаться и увидеть все приложение.

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


    1. var1m Автор
      22.12.2021 12:25

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

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

      А если зарегаться может любой то смысла большого нет. Всегда можно зарегаться и увидеть все приложение.

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

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

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


      1. Korobei
        23.12.2021 16:52

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

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


        1. var1m Автор
          24.12.2021 11:08

          Но это же не значит, что системы теперь не надо зищищать.


          1. Korobei
            24.12.2021 17:26

            Это значит, что скорее всего security-through-obscurity не очень надёжная стратегия.

            Я за дополнительные уровни, но надо отдвать отчёт, что это не панацея.


  1. nin-jin
    23.12.2021 10:17
    +2

    Сколько ни говорили людям, что "защита через незнание" ни от чего не защищает, а они всё-равно прикрывают свои дырки фиговыми листами, называя это защитой.