Angular does a lot of XSS prevention for you. That creates a dangerous habit: people stop thinking about where HTML, URLs, and templates come from.
Most Angular apps I review are not vulnerable because someone forgot to escape a string in interpolation. They break because someone worked around Angular’s safety model to make a feature ship faster. Usually that means dynamic templates, innerHTML, or bypassSecurityTrust... used like a global off switch.
Here are the mistakes I see most often, and how I’d fix them.
Mistake #1: Assuming {{ userInput }} is the XSS problem
Plain interpolation is usually the safe part.
<p>{{ profile.bio }}</p>
Angular escapes this output. If profile.bio contains <script>alert(1)</script>, it will render as text, not execute.
That leads some teams to chase the wrong thing and ignore the actual risk: places where Angular treats data as HTML, a URL, a style, or executable template syntax.
Fix
Keep untrusted data in normal interpolation whenever possible.
Good:
<h2>{{ comment.author }}</h2>
<p>{{ comment.text }}</p>
Riskier:
<div [innerHTML]="comment.text"></div>
If you only need to display text, never upgrade it to HTML.
Mistake #2: Rendering user content with [innerHTML] without a real reason
This one is everywhere:
<div [innerHTML]="post.body"></div>
Angular sanitizes HTML in many cases, which helps, but developers often misunderstand what this means. Sanitization is not permission to dump arbitrary user HTML into the DOM forever. You are still expanding your attack surface and making future mistakes easier.
For example, somebody later decides some tags are getting stripped and “fixes” it with bypassSecurityTrustHtml. That is usually where the real XSS starts.
Fix
Prefer structured rendering over raw HTML.
If your content is markdown, render markdown safely on the server or through a well-reviewed pipeline. If you only need formatting like bold, links, and lists, whitelist those features explicitly instead of allowing arbitrary HTML blobs.
If you must use [innerHTML], treat the source as hostile and keep Angular’s sanitizer in place:
<div [innerHTML]="safeHtmlFromBackend"></div>
And in TypeScript, do not “upgrade” it to trusted HTML unless you have already sanitized it yourself with a strict policy.
Mistake #3: Using bypassSecurityTrustHtml() on untrusted content
This is the Angular footgun.
I’ve seen code like this in production more times than I’d like:
import { DomSanitizer } from '@angular/platform-browser';
constructor(private sanitizer: DomSanitizer) {}
renderBio(input: string) {
return this.sanitizer.bypassSecurityTrustHtml(input);
}
That disables Angular’s HTML sanitization for input. If input is attacker-controlled, you just invited XSS in through the front door.
The same warning applies to:
bypassSecurityTrustHtmlbypassSecurityTrustScriptbypassSecurityTrustStylebypassSecurityTrustUrlbypassSecurityTrustResourceUrl
These APIs are for rare cases where you have established trust through a separate security review and validation path. They are not cleanup functions.
Fix
Don’t use bypass APIs on untrusted input. If you see them in code review, assume danger until proven otherwise.
Bad:
trustedHtml = this.sanitizer.bypassSecurityTrustHtml(userSuppliedHtml);
Better:
html = userSuppliedHtml;
<div [innerHTML]="html"></div>
That keeps Angular sanitization active.
If a business requirement truly needs richer HTML, create a dedicated sanitization layer with a strict allowlist, then document exactly why the content can be trusted before it ever reaches bypassSecurityTrustHtml().
Mistake #4: Building templates from strings
This is where “template injection” gets real.
Angular templates are not just HTML. They include bindings, expressions, directives, and event handlers. If you take attacker input and turn it into a template, you are no longer dealing with plain content rendering. You are compiling behavior.
A dangerous pattern looks like this conceptually:
const template = `<div>${userControlled}</div>`;
Or older/dynamic setups that compile templates at runtime from database content, CMS fields, or admin-configured “widgets”.
If attacker input lands in Angular template syntax, things get ugly fast:
{{ dangerousExpression }}
or
<button (click)="steal()">Click me</button>
Modern Angular is much stricter than AngularJS, but dynamic template compilation is still a terrible idea unless you absolutely control the source.
Fix
Never compile user-controlled strings as Angular templates.
Use configuration-driven rendering instead of template-driven rendering. For example, don’t store HTML or Angular snippets in the database. Store data and render known components based on that data.
Bad idea:
{
"widgetTemplate": "<promo-banner [message]=\"user.name\"></promo-banner>"
}
Safer pattern:
{
"widgetType": "promo-banner",
"message": "Welcome back"
}
Then map that to known components in code:
switch (widget.widgetType) {
case 'promo-banner':
this.component = PromoBannerComponent;
this.inputs = { message: widget.message };
break;
}
This keeps control of executable template logic inside your codebase.
Mistake #5: Treating “admin-only content” as trusted
I hear this one a lot: “Only admins can edit that HTML.”
That is not a trust boundary. Admin accounts get phished. Internal users make mistakes. Stored XSS in an admin panel is often worse than public XSS because it runs in high-privilege sessions.
If your CMS, marketing editor, support note system, or feature flag dashboard accepts HTML and your Angular app renders it, that content is untrusted unless it has gone through a deliberate sanitization process.
Fix
Apply the same rules to admin-authored content as public content unless there is a documented and reviewed trust model.
That means:
- sanitize before rendering
- avoid bypass APIs
- prefer structured content over raw HTML
- isolate especially risky content where possible
If the content really must be powerful, consider serving it from a separate origin with a tight CSP. For CSP rollout details, see CSP Guide.
Mistake #6: Trusting URLs in bindings
Angular sanitizes many URL contexts, but developers still make bad assumptions around links, iframes, and resource URLs.
Example:
<a [href]="user.website">Website</a>
This is generally safer than raw DOM assignment, but you still need to think about what URLs your app should allow. If the value feeds an iframe or resource URL, the rules get stricter.
The real mess usually starts here:
videoUrl = this.sanitizer.bypassSecurityTrustResourceUrl(userInput);
That turns attacker input into a trusted resource location.
Fix
Validate URLs by scheme, host, and intended use before binding them.
Example:
isAllowedProfileUrl(input: string): boolean {
try {
const url = new URL(input);
return ['https:'].includes(url.protocol) &&
['example.com', 'profiles.example.com'].includes(url.hostname);
} catch {
return false;
}
}
Then only bind validated values:
profileUrl = this.isAllowedProfileUrl(input) ? input : '/invalid-link';
For resource URLs like iframes, be even stricter. In most apps, a hardcoded allowlist is the only sane option.
Mistake #7: Mixing direct DOM APIs into Angular code
Angular’s template and binding system gives you security controls for free. Direct DOM manipulation can bypass those controls.
Bad pattern:
constructor(private el: ElementRef) {}
ngAfterViewInit() {
this.el.nativeElement.querySelector('.output').innerHTML = this.userContent;
}
That skips Angular’s template sanitization path entirely.
Fix
Avoid ElementRef.nativeElement for rendering untrusted data. Bind in templates instead:
<div class="output" [innerHTML]="userContent"></div>
That at least keeps Angular’s sanitizer in play.
If you need lower-level DOM work, use Angular APIs like Renderer2 where possible and keep untrusted content out of dangerous sinks.
Mistake #8: Forgetting that CSP is backup, not the main fix
A decent Content Security Policy can reduce the blast radius of XSS, and I strongly recommend it for Angular apps. But CSP will not save a bad trust model or unsafe template compilation design.
If your app depends on inline scripts, random third-party code, or trusted HTML from users, your CSP gets weaker fast.
Fix
Use CSP as defense in depth after fixing the actual injection points.
A practical direction for Angular apps:
- avoid inline scripts
- avoid unsafe eval-like behaviors
- restrict script sources tightly
- use Trusted Types where supported if your app can adopt them
Angular’s own security model is documented well in the official docs: Angular security best practices.
For CSP design and rollout patterns, CSP Guide is useful.
A safer mental model
Here’s the rule I use when reviewing Angular code:
If data stays as text, Angular is usually helping you.
If data becomes HTML, a URL, a resource, or a template, stop and review it like a security boundary.
That one rule catches most of the bad decisions.
A short checklist for code reviews:
- Are we using
[innerHTML]? Why? - Are any
bypassSecurityTrust...APIs present? - Are we compiling or generating templates from strings?
- Are admin/CMS fields being treated as trusted?
- Are URLs validated before binding?
- Is anyone writing to
innerHTMLdirectly through DOM APIs? - Do we have CSP as a fallback layer?
Angular is pretty good at preventing basic XSS. Most vulnerabilities happen when developers decide they know better than the framework. That is the mistake I’d focus on fixing first.