Защита вашего приложения Angular от межсайтового скриптинга

В последнем посте этой серии статей о безопасности SPA мы рассказали о подделке межсайтовых запросов (CSRF) и о том, как Angular помогает вам в борьбе с этой проблемой.

Далее мы рассмотрим межсайтовый скриптинг (XSS) и встроенные средства защиты, которые вы получаете при использовании Angular.

Защита от межсайтового скриптинга (XSS)

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

Давайте вспомним пример вектора атаки.

Представьте себе слишком драматичный, но в остальном невинный сценарий:

  1. Веб-сайт позволяет добавлять комментарии к любимой K-драме.
  2. Агитатор добавляет комментарий <script>alert('Crash Landing on You stinks!');</script>.
  3. Этот ужасный комментарий сохраняется в базе данных как есть.
  4. Фанат К-драмы открывает сайт.
  5. Ужасный комментарий добавляется на сайт, добавляя тег <script></script> в DOM.
  6. Поклонник K-драмы возмущен предупреждением JavaScript о том, что их любимая K-драма «Crash Landing on You» отвратительна.

В этом примере у нас есть элемент <script> и пропущены шаги по добавлению элемента в DOM. В реальности загрязненные данные попадают в приложение различными путями. Добавление недоверенных данных в инъекционный поглотитель — функцию Web API, которая позволяет нам добавлять динамический контент в наши приложения — является основным виновником. Примеры поглотителей включают, но не ограничиваются ими:

  • методы добавления в DOM, такие как innerHTML, outerHTML
  • методы загрузки внешних ресурсов или перехода на внешние сайты по URL, такие как src или href для HTML элементов и свойство url для стилей
  • обработчики событий, такие как onmouseover и onerror с недопустимым значением src
  • глобальные функции, которые оценивают и/или выполняют код, такие как eval(), setTimeout().

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

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

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

Хранимый XSS

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

Отраженный XSS

При этой атаке вредоносный код проникает через HTTP-запрос, обычно через параметры URL. Предположим, сайт K-Drama принимает поисковый запрос через параметр URL, например:

https://myfavekdramas.com/dramas?search=crash+landing+on+you
Войти в полноэкранный режим Выйти из полноэкранного режима

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

Но что если агитатор создаст URL-адрес следующим образом?

https://myfavekdramas.com/dramas?search=<img src=1 onerror="alert('Doh!')"/>
Вход в полноэкранный режим Выход из полноэкранного режима

Вы можете подумать, что никогда не перейдете по такой ссылке! Кто бы мог?! Но давайте вспомним, что в предыдущем посте вы нажали на ссылку в спаме, чтобы отправить деньги своему школьному возлюбленному. Это не в осуждение; никто не застрахован от перехода по сомнительным ссылкам. Кроме того, агитаторы довольно хитры. Они могут использовать сокращение URL-адресов, чтобы скрыть риск.

XSS на основе DOM

В этой атаке агрессор использует преимущества веб-интерфейсов API. Атака происходит полностью внутри SPA, и она практически идентична отраженному XSS.

Допустим, наше приложение зависит от внешнего ресурса — приложение встраивает <iframe> для показа трейлеров к K-драмам и устанавливает атрибут iframe src на внешний сайт. Таким образом, наш код может выглядеть следующим образом.

<iframe src="{resourceURL}" />
Вход в полноэкранный режим Выход из полноэкранного режима

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

<iframe src="javascript:alert('Boo!')" />
Вход в полноэкранный режим Выход из полноэкранного режима

Ну, черт возьми, у нас проблемы.

Поддержка XSS в Angular

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

Давайте посмотрим несколько примеров того, как Angular защищает нас от XSS.

Angular автоматически экранирует значения

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

@Component({
  selector: 'app-comments'
  template: `
    <p *ngFor="let comment of comments | async">
      {{comment}}
    <p>
  `
})
export class CommentsComponent implements OnInit {
  public comments: Observable<string[]>;

  constructor(private commentsService: CommentsService) { }

  public ngOnInit(): void {
    this.comments = this.commentsService.getComments();
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

Когда вы добавляете значения через интерполяцию в шаблоны (используя синтаксис {{}}), Angular автоматически экранирует данные. Поэтому комментарий:

<a href="javascript:alert('Crash Landing on You stinks!')">Click to win a free prize!</a>
Войти в полноэкранный режим Выйти из полноэкранного режима

отображается точно так же, как то, что написано выше в качестве текста. Это по-прежнему ужасный комментарий и недружелюбный по отношению к фанатам «Crash Landing on You», но он не добавляет элемент якоря в приложение. Это замечательно, потому что даже если бы атака была более вредоносной, она все равно не выполнила бы никаких действий.

Angular автоматически санирует значения

Допустим, мы хотим отображать комментарии, сохраняя любую безопасную разметку, которую вводит пользователь. У нас уже есть два вредоносных комментария, чтобы начать на зыбкой почве:

  1. <a href="javascript:alert('Crash Landing on You stinks!')">Click to win a free prize!</a>
  2. <img src=1 onerror="alert('Doh!')"/>

Затем поклонник K-Drama добавляет новый комментарий с безопасной разметкой.

<strong>It's a wonderful drama! The best!</strong>
Вход в полноэкранный режим Выход из полноэкранного режима

Поскольку CommentsComponent использует интерполяцию для заполнения комментариев, комментарии будут отображаться в браузере в виде текста, подобного этому:

Это не то, что нам нужно! Мы хотим интерпретировать HTML и разрешить текст <strong>, поэтому мы изменим наш шаблон компонента, чтобы привязать его к свойству HTML innerHTML.

<p 
  *ngFor="let comment of comments | async" 
  [innerHTML]="comment"
> 
<p>
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь на сайте отображается только второй комментарий, правильно отформатированный таким образом:

Первый комментарий с тегом anchor не отображает оповещение при нажатии! Второй комментарий с атакой в обработчике onerror показывает только битое изображение и не выполняет код ошибки! Angular не публикует список небезопасных тегов. Тем не менее, мы можем заглянуть в кодовую базу и увидеть, что Angular считает такие теги, как form, textarea, button, embed, link, style, template подозрительными и может вообще удалить тег или удалить определенные атрибуты/дочерние элементы.

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

Обрабатывая значения «по-ангуляровски», наше приложение надежно защищено от проблем с безопасностью! Успехов!

Обход проверок безопасности Angular

🚨 Вот драконы! 🚨

Будьте осторожны при обходе встроенного механизма безопасности; если вам это необходимо, внимательно прочитайте этот раздел:

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

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

Примечание — есть лучшие способы работы с изображениями и обработкой ошибок в Angular, если ваше приложение не должно быть настолько динамичным. Рассмотрите возможность использования элемента изображения и привязки к (error), или даже HostListener. Или есть много обучающих примеров, как чисто обработать это с помощью директивы. Действительно, есть много лучших вариантов. Вы всегда должны предпочитать стандартный путь Angular, используя строительные блоки Angular. Код будет намного проще поддерживать.

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

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

@Component({
  selector: 'app-trustworthy-image',
  template: `
    <section [innerHTML]="html"
  `
})
export class TrustworthyImageComponent {
  public html = `<img src=1 onerror="alert('Doh!')"/>`;
}
Вход в полноэкранный режим Выход из полноэкранного режима

В браузере вы видите разбитое изображение, и не появляется никакого предупреждения.

Мы можем использовать класс DomSanitzer в @angular/platform-browser, чтобы пометить значения как безопасные. Класс DomSanitizer имеет встроенные методы санации для четырех типов контекстов:

  1. HTML — привязка для добавления дополнительного содержимого, как в этом примере с изображением innerHTML
  2. Стиль — привязка стилей для придания сайту большего шарма.
  3. URL — привязка URL, например, когда вы хотите перейти на внешний сайт в теге якоря.
  4. URL ресурса — привязка URL, которые загружаются и выполняются как код.

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

  1. bypassSecurityScript

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

Давайте посмотрим, как выглядит этот компонент, когда мы помечаем значение HTML как доверенное.

@Component({
  selector: 'app-trustworthy-image',
  template: `
    <section [innerHTML]="html"
  `
})
export class TrustworthyImageComponent {
  public html = `<img src=1 onerror="alert('Doh!')"/>`;
  public safeHtml: SafeHtml;

  constructor(sanitizer: DomSanitizer) {
    this.safeHtml = sanitizer.bypassSecurityTrustHtml(this.html);
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь, если вы просмотрите это в браузере, вы увидите разбитое изображение и всплывающее предупреждение. Успех? Возможно…

Давайте рассмотрим пример с URL ресурса, например, пример XSS на основе DOM, где мы привязываем URL источника iframe.

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

@Component({
  selector: 'app-video',
  template: `
    <iframe [src]="linky" width="800px" height="450px"
  `
})
export class VideoComponent {

  // pretend this is from an external source
  public linky = '//videolink/embed/12345';
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Angular остановит вас прямо здесь. 🛑

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

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

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

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

Затем мы пометим URL видео как доверенный и привяжем его в шаблоне. Ваш VideoComponent изменится следующим образом:

@Component({
  selector: 'app-video',
  template: `
    <iframe [src]="safeLinky" width="800px" height="450px"
  `
})
export class VideoComponent {

  // pretend this is from an external source
  public videoId = '12345';
  public safeLinky!: SafeResourceUrl;

  constructor(private sanitizer: DomSanitizer) {
    this.safeLinky = sanitizer.bypassSecurityTrustResourceUrl(`//videolink/embed/${this.videoId}`)
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь вы сможете показывать трейлеры K-драм на своем сайте в iframe гораздо более безопасным способом.

Это невозможно подчеркнуть. Убедитесь, что вы доверяете значениям, прежде чем обходить встроенные средства защиты, которые вы получаете от Angular!

Отлично! Ну что, мы закончили? Не совсем. Есть пара моментов, которые следует отметить.

Используйте опережающую компиляцию (AOT) для дополнительной безопасности

AOT-компиляция в Angular имеет дополнительные меры безопасности для инъекционных атак, таких как XSS. AOT-компиляция настоятельно рекомендуется для производственного кода и является методом компиляции по умолчанию начиная с Angular v9. Она не только более безопасна, но и повышает производительность.

С другой стороны, другой формой компиляции является Just-in-time (JIT). JIT использовался по умолчанию в старых версиях Angular. JIT компилирует код для браузера на лету, и этот процесс пропускает встроенную защиту безопасности Angular, поэтому лучше использовать AOT.

Не конкатенируйте строки для построения шаблонов

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

Например, у вас не будет встроенной защиты Angular, если вы попытаетесь динамически сконструировать шаблоны, сочетающие HTML с данными с помощью конкатенации строк, или попросите API создать полезную нагрузку с шаблоном, который вы каким-то образом внедрите в приложение. Ваши хитроумные взломы динамических компонентов могут стать причиной проблем с безопасностью.

Остерегайтесь конструирования элементов DOM без использования шаблонов Angular

Любая забава с использованием ElementRef или Renderer2 — идеальный способ вызвать проблемы с безопасностью. Например, вы можете навлечь на себя беду, если попытаетесь сделать что-то подобное.

@Component({
  selector: 'app-yikes',
  template: `
    <div #whydothis></div>
  `
})
export class YikesComponent implements AfterViewInit {

  @ViewChild('whydothis') public el!: ElementRef<HTMLElement>;

  // pretend this is from an external source
  public attack = '<img src=1 onerror="alert('YIKES!')"';

  constructor(private renderer: Renderer2) { }

  public ngAfterViewInit(): void {

    // danger below!
    this.el.nativeElement.innerHTML = this.attack;
    this.renderer.setProperty(this.el.nativeElement, 'innerHTML', this.attack);
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

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

Явная санация данных

Класс DomSanitizer также имеет метод для явной санации значений.

Допустим, у вас возникла законная необходимость использовать ElementRef или Render2 для построения DOM в коде. Вы можете санитировать значение, которое добавляете в DOM, используя метод sanitize(). Метод sanitize() принимает два параметра, контекст безопасности для санирования и значение. Контекст безопасности — это перечисление, соответствующее контексту безопасности, перечисленному ранее.

Если мы переделаем YikesComponent для явной санации, код будет выглядеть следующим образом.

@Component({
  selector: 'app-way-better',
  template: `
    <div #waybetter></div>
  `
})
export class WayBetterComponent implements AfterViewInit {

  @ViewChild('waybetter') public el!: ElementRef<HTMLElement>;

  // pretend this is from an external source
  public attack = '<img src=1 onerror="alert('YIKES!')"';

  constructor(private renderer: Renderer2, private sanitizer: DomSanitizer) { }

  public ngAfterViewInit(): void {

    const cleaned = this.sanitizer.sanitize(SecurityContext.HTML, this.attack);
    this.renderer.setProperty(this.el.nativeElement, 'innerHTML', cleaned);
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь у вас есть изображение без потенциально опасного кода.

Рассмотрим доверенные типы

Еще один встроенный механизм безопасности в Angular — это настройка и использование политики безопасности содержимого (Content Security Policy, CSP). CSP — это особый заголовок безопасности HTTP, который мы рассматривали в первом посте, чтобы помочь установить базовые механизмы безопасности.

Angular имеет встроенную поддержку для определения политик для CSP, называемых доверенными типами. Доверенные типы — это отличный способ добавить дополнительные средства защиты от XSS в ваше приложение Angular, но пока они не поддерживаются во всех основных браузерах. Если вы хотите узнать больше о настройке CSP Trusted Types для SPA, ознакомьтесь с этой замечательной статьей из блога Auth0 — Securing SPAs with Trusted Types.

Узнайте больше о XSS, доверенных типах и создании приложений с помощью Angular

В этой серии статей мы узнали о веб-безопасности, распространенных веб-атаках и о том, как встроенные в Angular механизмы безопасности защищают нас от случайных атак.

Если вам понравился этот пост, возможно, вас заинтересуют эти ссылки.

  • Документация по безопасности от Angular
  • Как построить микрофронтенды с помощью федерации модулей в Angular
  • Три способа конфигурирования модулей в вашем приложении Angular
  • Защита от XSS с помощью CSP
  • Защита SPA с помощью доверенных типов

Не забывайте следить за нами в Twitter и подписываться на наш канал YouTube, чтобы получить еще больше отличных уроков. Мы также будем рады услышать вас! Пожалуйста, оставляйте комментарии ниже, если у вас есть вопросы или вы хотите рассказать, какой учебник вы хотели бы увидеть следующим.

Оцените статью
devanswers.ru
Добавить комментарий