保护你的Angular应用程序免受跨站脚本攻击

549 阅读12分钟

在这个SPA安全系列的最后一篇文章中,我们介绍了跨站请求伪造(CSRF)以及Angular如何用缓解技术帮助你。

SPA网页安全系列的帖子
1.保护你的SPA不受安全问题困扰
2.保护你的SPA免受常见的网络攻击
3.保护你的Angular应用程序免受跨站请求伪造的影响
4.保护你的Angular应用程序免受跨站脚本攻击

接下来,我们将深入研究跨站脚本(XSS),并看看你在使用Angular时得到的内置安全防护。

跨站脚本(XSS)保护

本系列的第二篇文章中,我们介绍了跨站脚本(XSS)的概况。总之,你了解到,当代码污染了数据,而你的应用程序没有提供保护措施来防止代码运行时,就会发生XSS。

让我们回顾一下攻击载体的例子:

想象一下这样一个过于戏剧化但却无辜的场景。

  1. 一个网站允许你添加关于你喜欢的K-Drama的评论。
  2. 一个煽动者添加了评论<script>alert('Crash Landing on You stinks!');</script>
  3. 这条糟糕的评论被原封不动地保存在数据库中。
  4. 一个K-Drama粉丝打开了这个网站。
  5. 这条可怕的评论被添加到网站上,并在DOM上添加了<script></script> 标签。
  6. K-Drama的粉丝被这个JavaScript警告激怒了,说他们最喜欢的K-Drama,Crash Landing on You,太臭了。

在这个例子中,我们有一个<script> 元素,并掩盖了将该元素追加到DOM的步骤。在现实中,被污染的数据会以各种方式被拉入应用程序。在注入汇中添加不受信任的数据--一个允许我们向应用程序添加动态内容的Web API函数--是一个主要的罪魁祸首。水槽的例子包括,但不限于。

  • 附加到DOM的方法,如innerHTMLouterHTML
  • 加载外部资源或通过URL导航到外部网站的方法,如HTML元素的srchref 和样式的url 属性
  • 事件处理程序,如:onmouseoveronerror 与一个无效的src
  • 评估和/或运行代码的全局函数,如eval()setTimeout()

正如你所看到的,该漏洞有许多载体。在构建动态网络应用时,这些汇中的许多都有合法的用例。由于这些汇是网络应用程序功能所必需的,我们必须通过转义和消毒来使用可信的数据。

这仍然是一个高水平的总结。细致的是,XSS漏洞在攻击的不同方式上可以说是相当狡猾。

有不同的XSS攻击,每一种都有稍微不同的攻击载体。我们将简要地介绍三种攻击方式。

存储的XSS

在这种类型的XSS中,攻击被保存在某个地方,如数据库中。我们在上面的例子中重述了存储的XSS,其中一个煽动者的可怕评论与script 标签持续存在于数据库中,并通过在警报中显示不友好的评论来毁掉别人的一天。

反射的XSS

在这种攻击中,恶意代码通过HTTP请求潜入,通常是通过URL参数。假设K-Drama网站通过URL参数获取一个搜索词,如:

https://myfavekdramas.com/dramas?search=crash+landing+on+you

然后,该网站采取搜索词并将其显示给用户,同时调用后台运行搜索。

但是,如果一个煽动者构建了一个像这样的URL呢?

https://myfavekdramas.com/dramas?search=<img src=1 onerror="alert('Doh!')"/>

你可能认为你永远不会浏览到这样的链接!谁会?谁会呢?但我们要记住,在以前的文章中,你确实点击了垃圾邮件中的链接,给你的高中恋人寄钱。这并不意味着是一种判断;没有人能够免于点击可疑的链接。此外,煽动者也很狡猾。他们可能使用URL缩短器来掩盖风险。

基于DOM的XSS

在这种攻击中,煽动者利用了Web APIs。攻击完全发生在SPA中,它与反射式XSS基本相同。

假设我们的应用程序依赖于一个外部资源--应用程序嵌入了一个<iframe> ,用于显示K-Dramas的预告片,并将iframe'ssrc 属性设置为一个外部网站。因此,我们的代码可能看起来像这样。

<iframe src="{resourceURL}" />

我们通常调用第三方服务来获取资源的URL,但煽动者已经渗透到这个第三方服务中,现在控制了返回的资源URL,使我们的代码看起来像这样:

<iframe src="javascript:alert('Boo!')" />

好吧,该死的,我们遇到了一些问题。

Angular中的XSS支持

幸运的是,Angular有很多内置的安全保护措施。它把所有的值都默认为可疑的和不可信任的,这非常有帮助,因为该框架会自动保护我们,防止在我们的应用程序中无意地产生漏洞。Angular会自动删除任何script 标签,所以我们不必担心原来的假设例子。

让我们看看Angular如何保护我们免受XSS攻击的一些例子。

Angular自动转义值

Web应用程序通过调用API来获得评论列表,然后将评论添加到模板中,从而实现像存储XSS例子中的评论功能。在Angular中,一个极其简化的评论组件可能看起来像这样。

@Component({
  selector: 'app-comments'
  template: `
    <p *ngFor="let comment of comments | async">
      {{comment}}
    <p>
  `
})
export class CommentsComponent implements OnInit {
  public comments: Observable<string[]>;
  
  constructor(private commentsService: CommentsService) { }

  public ngOnInit(): void {
    this.comments = this.commentsService.getComments();
  }
}

只有当Web应用将所有的值都视为可信的,并将它们直接添加到模板中时,XSS攻击矢量才会起作用,例如当Web应用没有首先转义或对值进行消毒。幸运的是,Angular会自动做到这两点。

当你在模板中通过插值添加数值时(使用{{}} 语法),Angular会自动转义数据。因此,注释。

<a href="javascript:alert(\'Crash Landing on You stinks!\')">Click to win a free prize!</a>

显示的内容与上面写的文字完全一样。这仍然是一个糟糕的评论,对 "Crash Landing on You "的粉丝也不友好,但它并没有在应用程序中添加锚元素。这很厉害,因为即使攻击更加恶意,它仍然不会执行任何行动。

Angular自动对数值进行消毒

假设我们想显示评论,保留用户输入的任何安全标记。我们已经有了两个恶意的评论,让我们在不稳定的基础上开始。

  1. <a href="javascript:alert(\'Crash Landing on You stinks!\')">Click to win a free prize!</a>
  2. <img src=1 onerror="alert('Doh!')"/>

然后,一个K-Drama的粉丝添加了一个新的评论,上面有安全标记。

<strong>It's a wonderful drama! The best!</strong>

因为CommentsComponent 使用插值来填充评论,评论将在浏览器中显示为这样的文本。

这不是我们想要的!我们想解释HTML,并允许<strong> 文本,所以我们改变我们的组件模板,将其与HTMLinnerHTML 属性绑定。

<p 
  *ngFor="let comment of comments | async" 
  [innerHTML]="comment"
> 
<p>

现在,网站只显示第二个评论的正确格式,像这样。

第一条带有anchor 标签的评论在点击时并不显示警报!在onerror 处理程序中带有攻击的第二条评论只显示破损的图片,并没有运行错误代码!Angular并没有公布不安全标签的列表。不过,我们还是可以偷偷看一下代码库,看到Angular认为诸如formtextareabuttonembedlinkstyletemplate 等标签是可疑的,可能会完全删除该标签或删除特定的属性/子元素。

正如我们前面所学到的,消毒会删除可疑的代码,同时保留安全的代码。Angular会自动将不安全的属性从安全元素中剥离出来。你会在控制台看到一个警告,让你知道Angular把内容清理掉了。

通过 "以Angular的方式 "处理数值,我们的应用程序得到了很好的保护,避免了安全问题的困扰成功了!

Giphy of Success baby meme

绕过Angular的安全检查

🚨**这里有龙!**🚨

小心绕过内置的安全机制;如果你需要,请仔细阅读本节。

如果你需要绑定Angular认为不安全的可信值,怎么办?你可以将值标记为受信任,并绕过安全检查。

让我们看一下带有错误处理程序的图片的例子。假设有一个合法的需求来绑定具有动态错误处理功能的图像,而不是来自于一个煽动者的值。

旁注--如果你的应用程序不需要如此动态,在Angular中还有更好的方法来处理带有错误处理的图像。可以考虑使用一个图像元素并绑定到(error) ,甚至是HostListener 。或者,有很多使用Directive干净利落地处理的教程案例。事实上,有很多更好的选择。你应该总是倾向于使用Angular构建块的标准Angular方式。代码会更容易支持。

回到这个例子,然后。在上面的例子中,我们看到错误处理程序没有运行。Angular把它剥离出来了。我们需要将代码标记为可信任,以便错误代码能够运行。

你的组件代码可能看起来像这样:

@Component({
  selector: 'app-trustworthy-image',
  template: `
    <section [innerHTML]="html"
  `
})
export class TrustworthyImageComponent {
  public html = `<img src=1 onerror="alert('Doh!')"/>`;
}

你在浏览器中看到破损的图片,但没有弹出警报。

我们可以使用@angular/platform-browser 中的DomSanitzer 类,将值标记为安全。DomSanitizer 类对四种类型的上下文有内置的消毒方法:

  1. HTML - 绑定添加更多的内容,就像这个innerHTML 的图片例子一样
  2. 样式 - 绑定样式,为网站添加更多亮点
  3. URL - 绑定URL,比如当你想在锚标签中导航到一个外部网站时
  4. 资源URL--作为代码加载和运行的绑定URL

为了将值标记为可信任和安全使用,你可以注入DomSanitizer ,并使用以下适合安全上下文的方法之一来返回一个标记为安全的值:

  1. bypassSecurityHTML
  2. bypassSecurityScript
  3. bypassSecurityTrustStyle
  4. bypassSecurityTrustUrl
  5. bypassSecurityTrustResourceUrl

这些方法返回相同的输入,但通过将其包裹在一个安全的等价的消毒类型中而被标记为可信。

让我们看看当我们把HTML值标记为可信任时,这个组件是什么样子。

@Component({
  selector: 'app-trustworthy-image',
  template: `
    <section [innerHTML]="html"
  `
})
export class TrustworthyImageComponent {
  public html = `<img src=1 onerror="alert('Doh!')"/>`;
  public safeHtml: SafeHtml;

  constructor(sanitizer: DomSanitizer) {
    this.safeHtml = sanitizer.bypassSecurityTrustHtml(this.html);
  }
}

现在,如果你在浏览器中查看这个,你会看到破损的图片和一个弹出的警报。成功吗?也许吧...

Giphy of not sure if meme

让我们来看看一个有资源URL的例子,比如基于DOM的XSS例子,我们绑定了iframe 源的URL。

你的组件代码可能看起来像这样

@Component({
  selector: 'app-video',
  template: `
    <iframe [src]="linky" width="800px" height="450px"
  `
})
export class VideoComponent {

  // pretend this is from an external source
  public linky = '//videolink/embed/12345';
}

Angular会在这里阻止你。🛑

你会在控制台看到一个错误,说不能在资源URL中使用不安全的值。Angular识别出你正在尝试添加一个资源URL,并对你正在做的危险事情感到震惊。资源URL可以包含合法的代码,所以Angular无法对其进行消毒,这与我们上面的注释不同。

如果我们确定我们的链接是安全和值得信赖的(在这个例子中很值得商榷,但我们暂时不考虑这个问题),我们可以在做一些清理工作使资源URL更安全之后,将资源标记为值得信赖。

记住,我们应该只在知道值是值得信任的情况下使用bypassSecurityTrust... 方法。你绕过了内置的安全机制,使你自己暴露在安全漏洞之下!

我们不使用基于外部方API响应的整个视频URL,而是通过在我们内部定义视频主机URL并附加我们从外部方API响应中得到的视频ID来构建URL。这样,我们就不会完全依赖来自第三方的可能不可信的值。相反,我们将有一些措施来确保我们没有在URL中注入恶意代码。

然后我们将视频URL标记为可信的,并在模板中绑定它。你的VideoComponent ,变成了这样:

@Component({
  selector: 'app-video',
  template: `
    <iframe [src]="safeLinky" width="800px" height="450px"
  `
})
export class VideoComponent {

  // pretend this is from an external source
  public videoId = '12345';
  public safeLinky!: SafeResourceUrl;

  constructor(private sanitizer: DomSanitizer) {
    this.safeLinky = sanitizer.bypassSecurityTrustResourceUrl(`//videolink/embed/${this.videoId}`)
  }
}

现在你就可以在你的网站上以更安全的方式在iframe ,展示K-Dramas的预告片了。

这一点怎么强调都不为过。在绕过Angular提供的开箱即用的安全保护之前,请确保你信任这些值

很好!那么我们就完成了?并非如此。有几件事情需要注意。

使用提前编译(AOT)以获得额外的安全性

Angular的AOT编译对XSS等注入攻击有额外的安全措施。AOT编译被强烈推荐用于生产代码,并且自Angular v9以来一直是默认的编译方法。它不仅更安全,而且还能提高性能。

反过来说,另一种编译形式是及时编译(JIT)。JIT是Angular旧版本的默认方式。JIT为浏览器即时编译代码,这个过程跳过了Angular的内置安全保护,所以坚持使用AOT。

不要用串联字符串来构建模板

Angular信任模板代码,并且只使用插值来转义模板中定义的值。因此,如果你试图用一些聪明的方法来规避为组件定义模板的更常见的形式,你将不会受到保护。

例如,如果你试图用字符串连接的方式动态构建HTML和数据的模板,或者让API用你注入应用的模板产生一个有效载荷,那么你就不会有Angular的内置保护。你对动态组件的巧妙黑客行为可能会给你带来安全隐患。

小心在没有使用Angular模板的情况下构建DOM元素

你可能会尝试使用ElementRefRenderer2 ,任何有趣的业务都是导致安全问题的最佳途径。例如,如果你试图做这样的事情,你会把自己搞得很惨:

@Component({
  selector: 'app-yikes',
  template: `
    <div #whydothis></div>
  `
})
export class YikesComponent implements AfterViewInit {

  @ViewChild('whydothis') public el!: ElementRef<HTMLElement>;
  
  // pretend this is from an external source
  public attack = '<img src=1 onerror="alert(\'YIKES!\')"';

  constructor(private renderer: Renderer2) { }

  public ngAfterViewInit(): void {
    
    // danger below!
    this.el.nativeElement.innerHTML = this.attack;
    this.renderer.setProperty(this.el.nativeElement, 'innerHTML', this.attack);
  }
}

像这样的事情在一个花哨的自定义指令中可能很诱人,但再想想吧此外,像这样直接与DOM交互并不是Angular的最佳做法,甚至超越了它可能存在的任何安全问题。创建和使用Angular模板始终是明智的选择。

明确地对数据进行消毒

DomSanitizer 类也有一个方法可以明确地对数值进行消毒。

假设你在代码中使用ElementRefRender2 来构建DOM的合法需求。你可以使用sanitize() 方法对你添加到DOM中的值进行消毒。sanitize() 方法需要两个参数,用于消毒的安全环境和值。安全上下文是一个与前面列出的安全上下文相匹配的枚举。

如果我们重做YikesComponent ,明确地进行消毒处理,代码看起来像这样。

@Component({
  selector: 'app-way-better',
  template: `
    <div #waybetter></div>
  `
})
export class WayBetterComponent implements AfterViewInit {

  @ViewChild('waybetter') public el!: ElementRef<HTMLElement>;
  
  // pretend this is from an external source
  public attack = '<img src=1 onerror="alert(\'YIKES!\')"';

  constructor(private renderer: Renderer2, private sanitizer: DomSanitizer) { }

  public ngAfterViewInit(): void {
    
    const cleaned = this.sanitizer.sanitize(SecurityContext.HTML, this.attack);
    this.renderer.setProperty(this.el.nativeElement, 'innerHTML', cleaned);
  }
}

现在,你将得到图像,而没有潜在的危险的代码标记在一起。