Code review example: bypassSecurityTrustHtml on user content: the sanitizer was the defense in Angular

Retired from the DiffDojo practice pool in October 2026; the review below is the canonical answer. An AI wrote this pull request for the task underneath. Read the diff and decide what you would say before you scroll to the review.

Angular · Security · intermediate

The task

Render user bios with markdown

  1. Render the user's bio with markdown formatting (bold, links, lists).
  2. Bios are user-provided and shown on public profile pages.
  3. User content must never be able to execute script.

Adds the UserBio component: parses the bio markdown and binds the resulting HTML. Uses Angular's DomSanitizer so the [innerHTML] binding accepts the rendered markup without warnings.

The diff

src/app/components/user-bio.ts+16
1+ import { Component, Input, inject } from '@angular/core';
2+ import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
3+ import { marked } from 'marked';
4+
5+ @Component({
6+ selector: 'app-user-bio',
7+ template: `<div class="bio" [innerHTML]="bioHtml"></div>`,
8+ })
9+ export class UserBio {
10+ private sanitizer = inject(DomSanitizer);
11+ bioHtml: SafeHtml = '';
12+
13+ @Input() set bio(value: string) {
14+ this.bioHtml = this.sanitizer.bypassSecurityTrustHtml(marked.parse(value) as string);
15+ }
16+ }

The review

Issue 1: bypassSecurityTrustHtml on user content: the sanitizer was the defense

src/app/components/user-bio.ts, lines 10, 13, 14, 15

  1. bypassSecurityTrustHtml means "render this without protection". Angular sanitizes [innerHTML] bindings by default, that built-in sanitizer was the exact defense the third requirement needs, and the bypass switches it off for content users type.
  2. Markdown is not an escape mechanism. marked passes raw HTML through by default: a bio of <img src=x onerror="..."> comes out of marked.parse intact, gets blessed as safe, and runs in every visitor's browser. Stored XSS on a public page.
  3. The API name warned about it. Methods called bypassSecurityTrust* exist for content the developer authors, never for content users submit.

The fix: drop the bypass and the sanitizer entirely, bind the parsed string and Angular sanitizes it on render. (For richer markup control, sanitize with DOMPurify before binding.)

The fix

import { Component, Input } from '@angular/core';
import { marked } from 'marked';

@Component({
  selector: 'app-user-bio',
  template: `<div class="bio" [innerHTML]="bioHtml"></div>`,
})
export class UserBio {
  bioHtml = '';

  @Input() set bio(value: string) {
    // Angular sanitizes [innerHTML] bindings by default — no bypass.
    this.bioHtml = marked.parse(value) as string;
  }
}

Why it matters

Any bypassSecurityTrust* call → ask: "who authored this content?" If the answer is "a user", it's a vulnerability regardless of what transformed the content on the way.

Real-world stakes: one crafted bio and every profile visitor runs attacker script, session theft, account takeover, worm-style spread to more bios. Models suggest the bypass constantly because it makes Angular's sanitization warning go away, and making warnings go away looks like fixing them.

Read more: angular bypassSecurityTrustHtml xss, angular innerHTML sanitization, marked raw html xss

The verdict

Request changes. bypassSecurityTrustHtml on user content: the sanitizer was the defense

Try a live one

Angular PRs you have not seen the answer to.

Leave inline comments and a verdict, then see what you caught. Free, no signup.

Review an Angular PR →

How to review Angular code · All code review examples