Learn Angular DomSanitizer and bypassSecurityTrustHtml Safely

Oct 6, 2026SecurityEngineering
Summarize with AI: Google AI Claude ChatGPT Perplexity Grok

AI links open with a title + excerpt (these tools can't fetch the page themselves) — use "Copy full article" to paste the complete text for a fuller summary.

Share:

Introduction

This article demonstrates how we render Markdown blog posts in Angular, and why that needs bypassSecurityTrustHtml — a method whose name is a warning.

If you search for this method you will find two kinds of answers. "Never use it" and "here is how to make the error go away". Neither is useful. This article is about when it is genuinely correct, and how to know when it stops being correct.

What Angular Is Protecting You From

By default, Angular treats any HTML you bind with [innerHTML] as untrusted. It removes <script> tags, onclick attributes, javascript: URLs and everything else that could run code.

This is why you cannot simply bind converted Markdown. Run this:

const html = marked.parse(post.contentMarkdown, { async: false }) as string;

...bind the result, and Angular strips parts of it. Not always visibly. A <details> block or an embedded iframe in an old post quietly disappears, and nobody notices for a month.

bypassSecurityTrustHtml turns the sanitizer off for that one value.

this.renderedContent.set(this.sanitizer.bypassSecurityTrustHtml(html));

Which means the safety of your page now depends entirely on who can write that string.

The Only Question That Matters

Not "is this HTML dangerous" — you cannot inspect every post. The question is:

Who is allowed to write this field, and do I trust all of them?

That is a trust boundary. Answer it in writing before you call the method.

Features of a defensible use

  • The set of people who can write the field is small and known.
  • Writing it already requires authentication.
  • You can say what would have to change for the answer to become "no".

With the following steps, you can decide for your own case.

  1. Find every writer of the field.
  2. Write the boundary down.
  3. Re-check it whenever a feature touches that field.

Find Every Writer of the Field

When we imported the blog, the answer was easy. contentMarkdown came from a one-time WordPress import of our own old posts, run from a script on a developer machine. Nothing else wrote it. The content was effectively static data we had authored ourselves.

Then we built the admin blog editor. Now contentMarkdown is written directly by a form, over HTTP, at runtime.

The code did not change. The justification did.

Before and after — the same bypass, two different sets of writers

This is the part that is easy to miss, and it is the reason this article exists. bypassSecurityTrustHtml was called on the same line before and after. If you had reviewed the diff for the editor feature, you would have seen new components and a new endpoint — nothing touching the renderer at all. The dangerous change was invisible in the diff.

Write the Boundary Down

So we wrote it into CLAUDE.md, our architecture notes:

contentMarkdown is trusted content, but not because it is inert import data anymore — the admin blog editor writes this field directly, so the trust boundary is "only the single authenticated admin can write it," not "never user input."

Two sentences, and they do real work.

They record that the reason changed, so nobody assumes the old one still holds. They state the current rule in a form you can test: only the single authenticated admin can write it. And they make the next question obvious — if a second, less trusted person ever gets write access, this needs revisiting.

Is it acceptable today? Yes. The one person who can write that field is the person who owns the site. If they wanted to put a <script> tag on their own blog, they can do that anyway — it is their blog. There is no privilege for an attacker to gain here, because there is no higher privilege than the author.

Re-check It When a Feature Touches the Field

Here is the rule we follow now.

When a feature adds a new writer to a field that is rendered with a bypass, that feature must revisit the bypass. Not the renderer's own commit — the writer's commit.

Concretely, these would each change the answer for us:

  • A second editor account with fewer privileges. Now "the single admin" is false.
  • Accepting guest posts, even reviewed ones. Now a stranger writes the field.
  • An import from any feed or third-party source. Now nobody is writing it.
  • Letting comments render Markdown the same way. That one is not close — comments are anonymous input and must never be bypassed.

That last one is the useful contrast. The same method, in the same app, on a different field, would be a serious vulnerability. The method is not safe or unsafe. The field is.

The same call on two fields — safe on one, a vulnerability on the other

What We Would Do If the Answer Changed

If a less trusted writer ever gets access, the fix is not to remove the bypass and accept Angular stripping tags. It is to sanitize the HTML properly before it reaches Angular:

  • Convert Markdown to HTML as now.
  • Run it through an HTML sanitizer with an explicit allow-list of tags and attributes — DOMPurify is the usual choice.
  • Then bypass Angular's sanitizer, because you have already done the job with a library built for it.

The bypass stays. What changes is that something trustworthy now runs before it.

My suggestion: if you are reading this and cannot answer "who writes this field" in one sentence, you should be doing that now rather than later.

One Related Note

The CSP for this site includes 'unsafe-inline' in script-src, needed for the small pre-paint theme script. That weakens the backstop you would want behind a decision like this one.

Two deliberate trade-offs that touch the same risk should be written next to each other, so nobody evaluates one while forgetting the other. Ours are both in the same security section of the architecture notes.

Conclusion

In this article we learned that bypassSecurityTrustHtml is a statement about your data, not about your code. It is correct when a small, known, authenticated set of people write the field, and wrong the moment that set grows.

The practical habit is the one that saved us here: write the trust boundary down next to the code, and re-read it whenever a feature adds a new writer — because that change will not look like a security change in the diff.

Reference

#angular#domsanitizer#xss#markdown#marked#security

Comments

Be the first to comment.

Leave a comment

Never shown publicly.

Comments are reviewed before appearing publicly.