Description
Which @angular/* package(s) are the source of the bug?
common
Is this a regression?
Yes
Description
I recently enabled CSP with nonces for one of our web projects. After that, I started noticing that some styling wasn't always properly loaded. For example, I noticed some styling for our dialogs was sometimes missing. The screenshot below shows a test page with and without the issue:
I noticed that "media=print" was not replaced with "media=all" for the styles.css bundle in the incorrect version. It happened frequently, but not all the time. So it seemed to be some sort of timing issue. Eventually, I narrowed it down to three conditions that need to be met:
- CSP nonce is set (e.g. )
- Part of the styling has to be loaded from an external stylesheet, in our case a .css file provided by a CDN. The second part of the styling is contained in a local bundle (e.g. styles.css) and injected automatically by Angular.
- The external stylesheet has to take at least 50ms to load.
This might sound quite rare. But this issue is now surfacing frequently across seven web projects we run. I suspect many developers will stylesheets provided by external sources, amended by local stylesheets, and combined with CSP nonces.
It isn't easy to reproduce the issue because it requires a server that adds the correct CSP headers. I set up a temporary site at https://test-cspissue.theliberatorsimproveyourteam.com to demonstrate the issue. The source code for the Angular site is available at https://github.com/christiaanverwijs/angular-csp-issue. To reproduce:
- In Chrome, use the Developers Tools. Go to "network" and "throttle" to "3G Fast" (this is only to slow the load of the CDN stylesheet to at least 50ms). Also, disable the local cache. Alternatively, just reload the page a few times in a new tab or window (CTRL+F5/F5) and the issue will show up.
- Load https://test-cspissue.theliberatorsimproveyourteam.com
- Click 'Open dialog'
- If the issue is present, the dialog box will appear immediately below the button, and not centered on a darker background (as it should be by the node_modules/@angular/material/prebuilt-themes/indigo-pink.css that we include in the local bundle).
- If the issue is not present, the dialog box appears nicely in the middle of the page.
Note that this isn't an issue with Material Design, but with Angular. The issue just surfaced most visibly first with a Material Design component.
Please provide a link to a minimal reproduction of the bug
https://github.com/christiaanverwijs/angular-csp-issue
Please provide the environment you discovered this bug in (run ng version
)
Angular CLI: 17.1.0
Node: 18.17.1
Package Manager: npm 9.6.7
OS: win32 x64
Angular: 17.1.0
... animations, cdk, cli, common, compiler, compiler-cli, core
... forms, language-service, material, material-moment-adapter
... platform-browser, platform-browser-dynamic, router
Package Version
---------------------------------------------------------
@angular-devkit/architect 0.1701.0
@angular-devkit/build-angular 17.1.0
@angular-devkit/core 17.1.0
@angular-devkit/schematics 17.1.0
@schematics/angular 17.1.0
rxjs 6.6.7
typescript 5.3.3
zone.js 0.14.3
Anything else?
I found that I could circumvent the issue by defining the "styles" as follows in angular.json. Not ideal, but it seems to work:
UPDATE JAN 23: As it happens, the solution below doesn't consistently resolve the issue on a production environment with a full-blown webapp.
"styles": [
{
// the first injected bundle isn't always loaded. so I added a dummy one.
"input": "src/less/styles.less",
"bundleName": "dummy"
},
{
"input": "node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
"bundleName": "material"
},
{
"input": "src/less/styles.less",
"bundleName": "app"
}
],
My hunch is that the script that is inserted into index.html by Angular when CSP nonces are active executes too quickly.
<script nonce="">(() => {
const children = document.head.children;
function onLoad() {this.media = this.getAttribute('ngCspMedia');}
for (let i = 0; i < children.length; i++) {
const child = children[i];
child.hasAttribute('ngCspMedia') && child.addEventListener('load', onLoad);
}
})();</script>