25 KiB
Angular
清单
验证以下内容:
- Angular 被视为客户端框架,不应提供服务器端保护
- 项目配置中禁用了脚本的 Sourcemap
- 不可信任的用户输入在使用模板之前始终进行了插值或清理
- 用户无法控制服务器端或客户端模板
- 不可信任的用户输入在被应用程序信任之前使用适当的安全上下文进行了清理
- 不要将不可信任的用户输入传递给 Angular 类,如
ElementRef
、Renderer2
和Document
,或其他 JQuery/DOM 漏洞
什么是 Angular
Angular 是一个强大的前端框架,广泛用于构建动态 Web 应用程序。它是开源的,并由 Google 维护。Angular 的一个关键特性是其使用 TypeScript,这是一种类型化的 JavaScript 超集,使代码更易于阅读、维护和调试。
Angular 的安全机制旨在防止常见的客户端漏洞,包括跨站脚本攻击(XSS)和开放重定向。然而,Angular 也可以在服务器端用于生成静态页面。因此,应从两个方面考虑 Angular 的安全性。
框架架构
为了更好地理解 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文件用于调试目的,它将生成的文件映射到其原始文件。因此,在生产环境中不建议使用它们。如果启用了sourcemap,它可以提高可读性,并通过复制Angular项目的原始状态来帮助文件分析。然而,如果禁用了它们,审阅者仍然可以通过手动搜索反安全模式来分析编译后的JavaScript文件。
此外,可以在浏览器开发者工具→Sources(或Debugger和Sources)→[id].main.js中找到带有Angular项目的编译后的JavaScript文件。根据启用的选项,该文件可能在末尾包含以下行//# sourceMappingURL=[id].main.js.map
,或者如果将hidden选项设置为true,则可能不包含。然而,如果对scripts禁用了sourcemap,测试将变得更加复杂,我们无法获取该文件。此外,可以在项目构建期间启用sourcemap,例如ng build --source-map
。
数据绑定
绑定是指组件与其对应视图之间的通信过程。它用于在Angular框架中传输数据。数据可以通过各种方式传递,例如通过事件、插值、属性或通过双向绑定机制。此外,数据还可以在相关组件(父子关系)之间和两个不相关的组件之间共享,使用Service功能。
我们可以通过数据流来分类绑定:
- 数据源到视图目标(包括插值、属性、属性、类和样式);可以在模板中使用
[]
或{{}}
应用; - 视图目标到数据源(包括事件);可以在模板中使用`()``应用;
- 双向绑定;可以在模板中使用
[()]
应用。
绑定可以应用于属性、事件和属性,以及源指令的任何公共成员:
类型 | 目标 | 示例 |
---|---|---|
属性 | 元素属性、组件属性、指令属性 | <img [alt]="hero.name" [src]="heroImageUrl"> |
事件 | 元素事件、组件事件、指令事件 | <button type="button" (click)="onSave()">Save |
双向绑定 | 事件和属性 | <input [(ngModel)]="name"> |
属性 | 属性(例外) | <button type="button" [attr.aria-label]="help">help |
类 | 类属性 | <div [class.special]="isSpecial">Special |
样式 | 样式属性 | <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">Click me</a>
//result
<a _ngcontent-pqg-c12="" class="e2e-trusted-url" href="javascript:alert()">Click me</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>
//result
<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>
//result
<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>
//result
-
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">
//result
Request 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>
结果是 <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
属性的作用域,使我们能够调用String构造函数并执行任意代码。
服务器端渲染(SSR)
与在浏览器的DOM中发生的客户端渲染(CSR)不同,Angular Universal负责对模板文件进行服务器端渲染。然后将这些文件传递给用户。尽管有这个区别,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
。关于后两个类的详细描述在开放重定向部分中给出。前两个类的主要区别在于,Renderer2
API在DOM元素和组件代码之间提供了一层抽象,而ElementRef
只是保存了对元素的引用。因此,根据Angular文档的说法,只有在需要直接访问DOM时,才应该使用ElementRef
API。
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中包含的脚本。除非显式设置keepScripts
为true
,否则jQuery.parseHTML()
方法不会运行解析的HTML中的脚本。然而,在大多数环境中,仍然可以间接执行脚本;例如,通过<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>
开放重定向
DOM接口
根据W3C文档,window.location
和document.location
对象在现代浏览器中被视为别名。这就是为什么它们具有一些方法和属性的类似实现,这可能导致开放重定向和使用javascript://
模式的DOM XSS攻击,如下所示。
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文档相同,这意味着可以使用常见的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安全性:权威指南(第1部分)
- Angular安全性:权威指南(第2部分)
- Angular安全性:权威指南(第3部分)
- Angular安全性:检查清单
- 工作区和项目文件结构
- 组件和模板简介
- 源映射配置
- 绑定语法
- Angular上下文:嵌套组件树和路由器出口的简单数据绑定
- 消毒和安全上下文
- GitHub - angular/dom_security_schema.ts
- Angular和AngularJS中的XSS
- Angular Universal
- DOM XSS
- Angular ElementRef
- Angular Renderer2
- Renderer2示例:在Angular中操作DOM - TekTutorialsHub
- jQuery API文档
- 如何在Angular中使用jQuery(当你绝对需要时)
- Angular Document
- Angular Location
- Angular Router