Repository navigation
Angular SSR heap OOM during mXSS stabilization, amplified by transient Domino ID indexing #70873
Description
Which @angular/* package(s) are the source of the bug?
core / platform-server (bundled Domino)
Is this a regression?
No
Description
Angular SSR can exhaust the Node.js heap when HTML rendered through a normal [innerHTML] binding contains a specific combination of active-formatting elements and repeated id attributes.
Example:
import { Component } from "@angular/core";
@Component({
selector: "app-root",
template: `<div [innerHTML]="content"></div>`,
})
export class App {
readonly content = attackerControlledHtml;
}On the server, Angular parses the value into an inert Domino document as part of HTML sanitization. For the minimized input, the sanitizer stabilization path performs multiple fragment parses.
During each parse, Domino maintains Document.byId / MultiId even though the document is only a temporary document used to build a fragment.
With the minimized candidate, the parser creates a large reconstructed DOM and adds more than 100k transient ID-index memberships before those nodes are moved out of the temporary document.
An equal-byte control changes only the attribute name from id to xx.
candidate: repeated id attributes
control: repeated xx attributes
Both inputs have the same size and produce the same element/attribute topology and serialization size, but only the id version enters Domino's ID index.
In the original 256 MiB validation, the 3,988-byte id candidate terminated all five workers:
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
I also tested larger heap limits with payloads calibrated for each limit:
| V8 old space | Request body | xx control |
id candidate |
|---|---|---|---|
| 256 MiB | 3,988 B | 5/5 healthy (original run; 4/5 on repeat) | 5/5 fatal V8 OOM |
| 512 MiB | 6,493 B | 3/3 HTTP 200 + healthy worker | 3/3 fatal V8 OOM |
| 1,024 MiB | 11,431 B | 3/3 HTTP 200 + healthy worker | 3/3 fatal V8 OOM |
More context : https://issuetracker.google.com/u/1/issues/563071374
Follow-up: two contributing memory costs
Further testing shows that Domino's temporary ID index is an amplifier, but not the only reason this reaches the heap limit.
Active-formatting reconstruction already creates a large DOM from the short input. During Angular's mXSS stabilization loop, the previous inert template also remains alive while the next fragment is parsed.
| Build | Request body | xx OOM |
id OOM |
|---|---|---|---|
| Original, repeat | 3,988 B | 1/5 | 5/5 |
| Lazy Domino ID index only | 3,988 B | 0/10 | 1/10 |
| Early mXSS template cleanup only | 3,988 B | 0/5 | 0/5 |
The lazy ID-index change greatly reduces the id failures, while clearing the previous inert template removes the OOM at this size in my tests.
Larger inputs can still exhaust the heap, including inputs without id. I haven't rerun the 512 MiB and 1,024 MiB cases with these changes.
See: https://github.com/SkyZeroZx/angular-ssr-mxss-domino-oom-poc
Activity
[-]Angular SSR can exhaust the Node.js heap through transient Domino ID indexing[/-][+]Angular SSR heap OOM during mXSS stabilization, amplified by transient Domino ID indexing[/+]perf(core): release intermediate inert trees while sanitizing HTML
perf(core): release intermediate inert trees while sanitizing HTML
alan-agius4 commentedon Sep 24, 2026
Thanks for the detailed analysis and reproduction!
The underlying DOM expansion here comes from the standard HTML5 active-formatting element reconstruction algorithm, where unclosed formatting tags followed by block elements legitimately expand into a large number of DOM nodes (
O(M * N)) in any spec-compliant HTML parser. As noted in your control benchmarks, inputs of this shape will eventually exhaust a constrained V8 heap regardless ofidattributes once the expanded DOM tree grows large enough.That said, both memory optimizations identified here are great performance and memory-hardening improvements to avoid unnecessary peak heap usage during SSR sanitization:
_sanitizeHtml(perf(core): release intermediate inert trees while sanitizing HTML #70879).byId/MultiIdindex so transient fragment-parsing documents do not pay for an ID index that is never queried (fix: build the id index only when an element is looked up by id domino#52).perf(core): release intermediate inert trees while sanitizing HTML
SkyZeroZx commentedon Oct 8, 2026
@alan-agius4 Should we consider backporting angular/domino#49 to supported versions? #70826 shows that less than 1 KB of HTML can exhaust a 256 MiB SSR heap.
This seems similar to the "Router OOM advisory" , since both abuse V8's handling of numeric properties. Could this also warrant a security advisory?