Skip to content

Commit 4fd8f61

Browse files
committed
fix(@angular/build): prevent HTML injection from inlined font CSS
Remote font CSS is embedded into a raw-text style element. Escape closing style tags so a compromised allowlisted response cannot terminate the style element and inject nonce-bearing markup.
1 parent e5fff87 commit 4fd8f61

2 files changed

Lines changed: 31 additions & 2 deletions

File tree

packages/angular/build/src/utils/index-file/inline-fonts.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,9 @@ export class InlineFontsProcessor {
150150
attrs.find(({ name, value }) => name === 'href' && hrefsContent.has(value));
151151
if (hrefAttr) {
152152
const href = hrefAttr.value;
153-
const cssContent = hrefsContent.get(href);
154-
rewriter.emitRaw(`<style>${cssContent}</style>`);
153+
const cssContent = hrefsContent.get(href) ?? '';
154+
// Prevent the CSS from terminating the generated raw-text style element.
155+
rewriter.emitRaw(`<style>${cssContent.replace(/<\/(?=style)/gi, '<\\/')}</style>`);
155156
} else {
156157
rewriter.emitStartTag(tag);
157158
}

packages/angular/build/src/utils/index-file/inline-fonts_spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import { htmlRewritingStream } from './html-rewriting-stream';
910
import { InlineFontsProcessor } from './inline-fonts';
11+
import { addNonce } from './nonce';
1012

1113
describe('InlineFontsProcessor', () => {
1214
describe('Google fonts', () => {
@@ -100,6 +102,32 @@ describe('InlineFontsProcessor', () => {
100102
`<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>`,
101103
);
102104
});
105+
106+
it('should escape closing style tags in inlined CSS', async () => {
107+
const inlineFontsProcessor = new InlineFontsProcessor({
108+
minify: true,
109+
});
110+
spyOn(inlineFontsProcessor, 'processURL').and.resolveTo(
111+
'body { color: red; }</StYlE><script>globalThis.attack = true;</script><style>',
112+
);
113+
114+
const html = await inlineFontsProcessor.process(`
115+
<html>
116+
<head>
117+
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
118+
</head>
119+
<body><app-root ngCspNonce="trusted-nonce"></app-root></body>
120+
</html>`);
121+
const htmlWithNonce = await addNonce(html);
122+
const { rewriter, transformedContent } = await htmlRewritingStream(htmlWithNonce);
123+
const elementNames: string[] = [];
124+
rewriter.on('startTag', ({ tagName }) => elementNames.push(tagName));
125+
await transformedContent();
126+
127+
expect(htmlWithNonce).toContain('body { color: red; }<\\/StYlE>');
128+
expect(elementNames.filter((name) => name === 'style')).toHaveSize(1);
129+
expect(elementNames).not.toContain('script');
130+
});
103131
});
104132

105133
describe('Adobe Typekit fonts', () => {

0 commit comments

Comments
 (0)