Using Angular innerHtml to display user-generated content without sacrificing security

Why do some of your styles no longer work when using [innerHtml] to show some HTML content? Angular comes with a built-in html sanitizer DomSanitizer, as a security feature, thats used whenever you use [innerHtml]. Its a great feature - but has a pretty annoying bug/feature in that if you have elements with inline styles, the styles wind up getting removed from your page. Judging by the amount of questions on how to turn it off - it clearly has its shortcomings. That said, I do appreciate the Angular devs building this in to protect the majority of us from doing something silly in their webapp.

The easiest approach is to disable DOMSanitizer and the recommended approach on Stack Overflow is to create a safeHtml pipe so that you could write <div [innerHtml]="content | safeHtml">. It's a few lines of code if you just want to drop it in - see the answer here.

But before jumping right into a solution that disables a security feature thats on by default, lets think about what we're doing for a second.

Why disable it at all?

The use case that I (and apparently many others) have is that I want to display user generated HTML content - ranging from small snippets to entire HTML pages. The issue is that DomSanitizer is stripping too much out of my HTML, and there's no way to configure its rules. In my case, I had <span style="background-color:red"> spans with inline style attributes as well as a <style> block.

DomSanitizer has its own rules about what is considered "safe" HTML. Its not wrong - I'm sure they have a good reason and they provide a safe default. As an example, Take a look at the source for DomSanitizer that shows what HTML attributes are "whitelisted". I have both <style> tags and inline styles on element - both of which it doesn't like and so it gets removed out of my HTML. No, I have no control over the source HTML.

🙁 Thats annoying. In this case, I kinda have to throw the baby out with the bathwater. I have to turn it off completely so that it works for what I need, but I lose all the Angular guardrails. No bueno.

bypassing DomSanitizer

DomSanitizer offers a few methods to allow you to bypass only certain parts of your content - but it doesn't quite work for user-generated HTML. Here's why.

sanitizer.bypassSecurityTrustScript(content) - content is expected to be javascript content and not HTML containing javascript content. Now it would be wonderful if I could keep this part because I don't expect my user-generated HTML to contain <script> tags (and they're cleaned out at the server-side) but I don't have the option of use the script sanitizer on my HTML content.

sanitizer.bypassSecurityTrustStyles(content) - content is expected to be CSS content, and not HTML containing a <style> tag. I can't use this method because my HTML contains style elements that are getting stripped. Welp. :shrug:

sanitizer.bypassSecurityTrustHtml(content) - content is expected to be an HTML snippet or an entire page and it leaves all your content untouched - which is NOT RECOMMENDED at all. Unfortunately, I have no other option around it.

So then, how can you stay safe from XSS attacks in Angular?

If you find yourself in this predicament where you have to bypass DomSanitizer - hopefully I can save you some time and show you what I've found.

Yes, you'll have to pull yet-another-dependency into your app, but the open-source JS community comes to the rescue. The following libraries were popular on npm & github, worked on both the server-side and client-side, with a configurable sanitizer using whitelists/blacklists and other cool features. Definitely worth checking out.

punkave/sanitize-html A popular option with a very succint and obvious name. Under recent active development.

cure53/dompurify
DOMPurify has a publicly available security audit report from 2015 which has just 2 major vulnerabilities for IE9. Also worth a read if you want to learn what a DOM Clobbering attack is. Check it out here.

how to use it in place of DomSanitizer

Here's what my safeHtml Pipe looks like:

import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import DOMPurify from 'dompurify';

@Pipe({
  name: 'safeHtml'
})
export class SafeHtmlPipe implements PipeTransform {

  constructor(protected sanitizer: DomSanitizer) {}

 public transform(value: any, type: string): any {
     const sanitizedContent = DOMPurify.sanitize(value);
     return angular.bypassSecurityTrustHtml(sanitizedContent);

  }
}

You can lean on an html-sanitizing library to clean your content first, so that you can bypass the sanitizer without losing the benefits of the XSS protection.

Wrapping up

Just a reminder - you can never trust the client/browser. Always sanitize on the server-side where you can guarantee that you don't save any malicious content.

If you're using nodejs on the server-side, then this will work perfectly for you. If you find yourself needing to sanitize html on the client-side however, bring in one of the sanitizing libraries so you get some protection from XSS attacks when a built-in like DomSanitizer doesn't meet your needs.

I'm interested to hear from other people who solved this problem - leave a comment if you took a different approach, or just disabled it entirely!

You might be interested in…

Menu