The Checklist
Checklist from here .
What is Angular
Angular 是一个 强大 和 开源 的前端框架,由 Google 维护。它使用 TypeScript 来增强代码的可读性和调试能力。凭借强大的安全机制,Angular 防止常见的客户端漏洞,如 XSS 和 开放重定向 。它也可以在 服务器端 使用,因此从 两个角度 考虑安全性非常重要。
Framework architecture
为了更好地理解 Angular 的基础知识,让我们了解其基本概念。
常见的 Angular 项目通常看起来像:
Copy 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 文件,并且默认情况下不隐藏:
Copy "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 漏洞变得越来越困难。数据处理有两种不同的场景:
插值或 {{user_input}}
- 执行上下文敏感编码并将用户输入解释为文本;
Copy //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"
- 根据提供的安全上下文执行清理。
Copy //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
:
STYLE
在将 CSS 绑定到 style
属性时使用;
URL
用于 URL 属性,例如 <a href>
;
RESOURCE_URL
作为加载并作为代码执行的 URL,例如在 <script src>
中。
漏洞
绕过安全信任方法
Angular 引入了一系列方法来绕过其默认的清理过程,并指示某个值可以在特定上下文中安全使用,如以下五个示例所示:
bypassSecurityTrustUrl
用于指示给定值是安全的样式 URL:
Copy //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:
Copy //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 树中的。
Copy //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 代码。
Copy //app.component.ts
this .trustedScript = this . sanitizer .bypassSecurityTrustScript ( "alert('bypass Security TrustScript')" );
//app.component.html
< script [innerHtml]="trustedScript"></script>
//结果
-
bypassSecurityTrustStyle
用于指示给定值是安全的 CSS。以下示例说明了 CSS 注入:
Copy //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
的示例:
Copy //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 的上下文、属性和变量。因此,模板注入攻击可能如下所示:
Copy //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()
方法:
Copy //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 注入漏洞,如下所示:
Copy //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 预防机制。
Copy //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 攻击:
Copy //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 属性来发生,如示例所示。
Copy //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 节点,然后可以将其插入到文档中。
Copy jQuery .parseHTML (data [ , context ] [ , keepScripts ])
如前所述,大多数接受 HTML 字符串的 jQuery API 将运行包含在 HTML 中的脚本。jQuery.parseHTML()
方法不会运行解析 HTML 中的脚本,除非 keepScripts
显式为 true
。然而,在大多数环境中,仍然可以间接执行脚本;例如,通过 <img onerror>
属性。
Copy //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
。它也可以用于将浏览器重定向到新页面。因此,控制该对象使我们能够利用开放重定向漏洞。
Copy //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 的文档。如果我们控制该方法,它可能是开放重定向攻击的一个入口。
Copy //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()
后,当前页面不会保存在会话历史中。然而,当我们控制该方法时,也可以利用开放重定向漏洞。
Copy //app.component.ts
...
export class AppComponent {
goToUrl () : void {
window . location .replace ( "http://google.com/about" )
}
}
window.open()
方法接受一个 URL,并将其识别的资源加载到新标签或窗口中。控制该方法也可能是触发 XSS 或开放重定向漏洞的机会。
Copy //app.component.ts
...
export class AppComponent {
goToUrl () : void {
window .open ( "https://google.com/about" , "_blank" )
}
}
Angular 类
根据 Angular 文档,Angular Document
与 DOM 文档相同,这意味着可以使用通用向量来利用 Angular 中的客户端漏洞。Document.location
属性和方法可能是成功开放重定向攻击的入口,如示例所示:
Copy //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()
。然而,我们无法使用它们进行重定向到外部域。例如:
Copy //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
类主要用于在同一域内导航,并不会给应用程序引入任何额外的漏洞:
Copy //app-routing.module.ts
const routes : Routes = [
{ path : '' , redirectTo : 'https://google.com' , pathMatch : 'full' }]
结果:http://localhost:4200/https:
以下方法也在域的范围内导航:
Copy const routes : Routes = [ { path : '' , redirectTo : 'ROUTE' , pathMatch : 'prefix' } ]
this . router .navigate ([ 'PATH' ])
this . router .navigateByUrl ( 'URL' )
参考文献