Skip to content

fix: escape raw text wherever a parser can read it as foreign content - #33

Open
SkyZeroZx wants to merge 1 commit into
angular:mainfrom
SkyZeroZx:fix/style-xss
Open

SkyZeroZx wants to merge 1 commit into
angular:mainfrom
SkyZeroZx:fix/style-xss

Conversation

@SkyZeroZx

@SkyZeroZx SkyZeroZx commented Jul 23, 2026 •

Copy link
Copy Markdown

When a <style> created through DOM APIs ends up inside an <svg> or <math> (content projection, Renderer2, NgComponentOutlet...), domino writes its text as raw text. But the browser parsing the SSR output sees that <style> inside SVG, so it becomes an SVG <style> and the text is parsed as markup:

<svg><g><style>/*<img src=x onerror=alert(1)>*/</style></g></svg>

You don't even need a </style> for this, the <img> alone breaks out of the SVG. The same happens with script, xmp, noembed and the other raw text elements.

With this change the text gets escaped whenever a parser reading our output could see the start tag inside SVG or MathML, and stays raw everywhere else, so CSS inside a <foreignObject> keeps working as before.

The HTML loop in serializeChildren() is the same as before, only svg/math subtrees go through a new serializeForeignContent() that keeps track of how the parser reads each element while serializing (integration points only in their own namespace, breakout tags, template contents with the same _host hop as fallbackRawContentTags()). Some trees built through DOM APIs don't survive a round trip through the parser, e.g. a stray end tag closing a foreign ancestor, a void element that isn't void in SVG, or a <table> inside a foster parented <svg>. When that can happen I keep escaping until the outermost <svg>/<math> is closed.

I also had to change how a <style> that can be read as SVG is escaped: escaping only its </style also broke the end tag of a nested <style>, and the parser stayed inside the SVG after the </svg>. Now all its markup gets escaped in that case.

I tested it with trees built through DOM APIs in Chrome, Firefox and WebKit, plus the Angular cases from the issue, and nothing got through. On regular HTML the overhead is around 2%, a bit more on pages with a lot of SVG.

The only downside I found is that with some invalid nesting (like an <svg> directly inside a <table>) a <style> can end up escaped even if the browser would read it as CSS. I think that's fine atm since it's always on the safe side.

Fixes angular/angular#69900
More context issuetracker.google.com/u/1/issues/537033759

@SkyZeroZx
SkyZeroZx force-pushed the fix/style-xss branch 5 times, most recently from d09dd18 to 1a88dea Compare July 24, 2026 21:07
@SkyZeroZx
SkyZeroZx marked this pull request as ready for review July 24, 2026 21:39
@SkyZeroZx

Copy link
Copy Markdown
Author

@alan-agius4 Could you please check this?

@SkyZeroZx

Copy link
Copy Markdown
Author

@alan-agius4 Sorry to bother you, but I think this should be included in a patch this week as it would be considered a security vulnerability.

@SkyZeroZx

Copy link
Copy Markdown
Author

@alan-agius4 This PR is ready for review ,. Could you take a look at it?

A parser assigns namespaces from the markup, not from our tree, so the text of
an HTML raw text element is escaped wherever a parser re-reading the output may
process its start tag in SVG or MathML content, and stays raw everywhere else,
including at HTML integration points.

The HTML loop of serializeChildren() stays as it was. svg and math subtrees go
through serializeForeignContent(), which tracks top-down how a parser reads
them: integration points only count in their own namespace, and template
contents and fallback raw text are covered. Raw text stays escaped once the
parser can leave the tree inside svg or math: when an end tag closes a
same-named foreign ancestor of an integration point, when a breakout tag pops a
nested svg or math element, when raw text is ended early by its own end tag,
after a void element, which is not void in foreign content, and at a table
start tag in an integration point of an svg or math element foster parented by
a table, where the table insertion mode pops past the integration point.

A style, script, xmp or plaintext element that may be read as foreign content
gets all of its markup escaped instead of the end tags that match it, since
those are real end tags of nested elements when it is read as foreign content.

Fixes angular/angular#69900
@SkyZeroZx SkyZeroZx changed the title fix: escape HTML style text in foreign content fix: escape raw text wherever a parser can read it as foreign content Nov 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

<style> text serialization under SVG in Angular SSR

1 participant