angular-cli icon indicating copy to clipboard operation
angular-cli copied to clipboard

inlineCritical CSS asynchronous loading method breaks with CSP

Open jpduckwo opened this issue 3 years ago • 34 comments

Bug Report

Affected Package

@angular/cli

Is this a regression?

Nay

Description

The method used for inlining critical css and asynchronously loading it, breaks and doesn't load the external stylesheet when you have a content security policy that doesn't include script-src 'unsafe-inline'. As the name suggests this isn't a very secure way of operating for various reasons such as script injection.

You can fix by disabling inlineCritical in the optimizations - however maybe there is a better way to load the styles, maybe in a JS file?

<link rel="stylesheet" href="styles.css" media="print" onload="this.media='all'">

It's the onload="this.media='all'" that breaks it

Minimal Reproduction

https://github.com/jpduckwo/ng12-csp-issue

run: ng serve

Exception or Error

Refused to execute inline event handler because it violates the following Content Security Policy directive: "default-src 'self'". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution. Note also that 'script-src' was not explicitly set, so 'default-src' is used as a fallback. image

Your Environment

Angular Version:


@angular-devkit/architect       0.1200.1
@angular-devkit/build-angular   12.0.1
@angular-devkit/core            12.0.1
@angular-devkit/schematics      12.0.1
@schematics/angular             12.0.1
rxjs                            6.6.7
typescript                      4.2.4

jpduckwo avatar May 20 '21 01:05 jpduckwo

I also faced this.

hardikpatel043 avatar May 20 '21 05:05 hardikpatel043

I also faced this issue. There are any options to disable this inline of critical css?

tiberiuzuld avatar May 20 '21 07:05 tiberiuzuld

Found how to disable the inlineCritical styles https://github.com/angular/angular-cli/commit/aa3ea885ed69cfde0914abae547e15d6d499a908 https://angular.io/guide/workspace-config#styles-optimization-options In angular.json in the build configuration Instead of

"optimization": true

put

"optimization": {
  "scripts": true,
  "styles": {
    "minify": true,
    "inlineCritical": false
  },
  "fonts": true
},

tiberiuzuld avatar May 20 '21 07:05 tiberiuzuld

We can probably solve this by changing the preload strategy of critters from media to default.

Let me reach out to the Chrome team so see if there are any drawbacks of doing so.

alan-agius4 avatar May 20 '21 09:05 alan-agius4

@alan-agius4 I think the "body" mode could work - it injects styles at the <link> location, then adds the <link rel=stylesheet href=css> just before </body>.

developit avatar May 20 '21 18:05 developit

@developit, thanks for the input. I had a chat with @janicklas-ralph, and mentioned that the default is idea although in some cases it can break CSS because of different CSS ordering. I’ll investigate a bit more.

alan-agius4 avatar May 20 '21 18:05 alan-agius4

@alan-agius4 another option would be to have the JS bundle itself flip those preloads to stylesheets (as a backup). It could scan for the preload/disabled <link> elements, add an onload listener, then force a synchronous load event if the sheet has already been loaded:

[].forEach.call(document.querySelectorAll('link[rel="stylesheet"][media="print"]'), n => {
  n.onload = () => n.media='';
  n.href += ''; // onload if not already loading
});

developit avatar May 20 '21 19:05 developit

An interesting article with an overview of this concept :

ghusta avatar May 27 '21 16:05 ghusta

Found how to disable the inlineCritical styles aa3ea88 https://angular.io/guide/workspace-config#styles-optimization-options In angular.json in the build configuration Instead of

"optimization": true

put

"optimization": {
  "scripts": true,
  "styles": {
    "minify": true,
    "inlineCritical": false
  },
  "fonts": true
},

I tried @tiberiuzuld's solution but didn't get it working. Any other workarounds?

AElmoznino avatar May 31 '21 09:05 AElmoznino

@AElmoznino that is the correct solution to restore the previous behaviour. However there may be multiple "optimization": true statements in your angular.json that you need to update. E.g. multiple configs for test / production etc.

jpduckwo avatar May 31 '21 09:05 jpduckwo

Thanks @jpduckwo, I didn't notice that 2nd optimization statement, got it working now!

AElmoznino avatar May 31 '21 10:05 AElmoznino

The workaround from @tiberiuzuld fixed the issue for me.

It might be a good idea to set up an integration test with a restrictive CSP and a basic app generated from the cli. In this way this sort of issue could be caught early. Angular should be able to work with this:

default-src 'none';script-src 'self';connect-src 'self';style-src 'self';form-action 'self';base-uri 'self';img-src 'self' data:;font-src 'self';

mjeffrey avatar Jun 30 '21 07:06 mjeffrey

We were facing the same issue. We had 2 optimization flags. The first build.options.optimization was set to false. I initially changed the 2nd one with the production configuration. The workaround didn't work. I then changed the first one (initially at false) to the workaround and it started working.

"build": {
  ...
  "options": {
    ...
	"optimization": {   // Originally set to false.
	  "scripts": true,
	  "styles": {
		"minify": true,
		"inlineCritical": false
	  },
	  "fonts": true
	},
	...
  },
  "configurations": {
	"production": {
		...
	  "optimization": {   // Originally set to true.
		"scripts": true,
		"styles": {
		  "minify": true,
		  "inlineCritical": false
		},
		"fonts": true
	  },
	  ...
    }
  }

Not sure if it's a bug, but I would expect optimization false to also implies inlineCritical:false. At least, it's working now.

MaximeMorin-Devolutions avatar Jul 28 '21 16:07 MaximeMorin-Devolutions

The necessary change, to actually skip the onload event listener in the ssr html is to provide the additional inlineCriticalCss property when using the ngExpressEngine:

server.engine('html', ngExpressEngine({ bootstrap: AppServerModule, inlineCriticalCss:false }))

simonmumenthaler avatar Aug 31 '21 06:08 simonmumenthaler

Just came across this situation with 12.2.7 where inlineCritical is causing my app's global stylesheet to not be applied.

Despite having no print MQs this default optimization setting is adding media=print to the link.

<link rel="stylesheet" ... href="styles.css" media="print">
"architect": ...
"build": ...
"configurations": {
  "dev": {
    ...
    "optimization": {
      "scripts": true,
      "styles": {
        "minify": true,
        "inlineCritical": false <--- fix
      },
      "fonts": true
    }

I added this as an answer to an older StackOverflow question if anyone is interested.

BenRacicot avatar Sep 26 '21 15:09 BenRacicot

I have similar but not equal problem Refused to execute inline event handler because it violates the following Content Security Policy directive: "script-src 'self' 'unsafe-eval'". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution. Note that hashes do not apply to event handlers, style attributes and javascript: navigations unless the 'unsafe-hashes' keyword is present. image

          "configurations": {
            "production": {
              "budgets": [
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "6kb"
                }
              ],
              "optimization": {
                "scripts": true,
                "styles": {
                  "minify": true,
                  "inlineCritical": false
                },
                "fonts": true
              },
              "outputHashing": "all",
              "sourceMap": false,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "serviceWorker": false,
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ]
            }
          },

pacocom avatar Nov 04 '21 10:11 pacocom

Hi @pacocom, it looks like the same problem. Maybe you need to apply the fix to other config areas of you angular config. Look for other "optimization" settings in your angular.json that are set to something other than false.

jpduckwo avatar Nov 04 '21 21:11 jpduckwo

It works now. Thanks.

pacocom avatar Nov 05 '21 07:11 pacocom

@alan-agius4 and maybe others..

What is the status of this issue? What i understand is that we have to disable the inline css feature, OR make our CSP more insecure. Both dont sound like great solutions.

Will there be a good fix to not require us to either sacrifice perf or security? If not, will there be documentation for angular with a CSP guide on how to create a Security performance balance?

sander1095 avatar Nov 23 '21 16:11 sander1095

disable the inline css feature, OR make our CSP more insecure

Just disable inline CSS.

External stylesheets worked for dial-up internet, IDK why people insist on complicating the most simple things when speeds are 100x what they used to be.

EDIT: Enjoy your bugs.

pauldraper avatar Nov 30 '21 14:11 pauldraper

@sander1095 One work-around is of course to add script-src 'unsafe-hashes' 'sha256-MhtPZXr7+LpJUY5qtMutB+qWfQtMaPccfe7QXtCcEYc=' This is not really more insecure, as it only allows the this.media='all' script to execute, which is the culprit of this issue.

PapaNappa avatar Dec 01 '21 14:12 PapaNappa

Hello, I'm on Angular 13.0.2 and I have the same problem as @pacocom. I tried to apply the fix to other config areas of my angular config (as suggested by @jpduckwo ) but still the same error. Any ideas?

silvia-giordano avatar Dec 17 '21 13:12 silvia-giordano

Can somebody point to the logic which extracts the 'critical css' and then renders it as inline? I mean maybe it's possible to customize that to include CSP nonces you have defined somewhere.

Rugshtyne avatar Mar 10 '22 16:03 Rugshtyne

I am trying to apply fix in Angular 13. Please post angular.json with fix. I changed it as suggested. But, I still have
body style="overflow: auto; position: static; touch-action: auto;" I didn't put that inline style there in the body tag. I guess Angular wants a quick page paint. Please post angular.json with fix. My angular.json file is the following. But, it doesn't stop inlining. angularjson.txt

rickz21 avatar Apr 16 '22 05:04 rickz21

@rickz21 try putting the optimizations in the build > options portion of the JSON

e.g.

...
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.app.json",
            "assets": [
...
              "src/assets"
            ],
            "styles": [
              "src/styles.scss"
            ],
            "scripts": [],
            "outputHashing": "all",
            "optimization": {
              "scripts": true,
              "styles": {
                "minify": true,
                "inlineCritical": false
              },
              "fonts": true
            },
...

jpduckwo avatar Apr 18 '22 23:04 jpduckwo

@jpduckwo thank you trying to help me. I did paste in the lines you posted within build.options but it didn't work for me. Angular still inserted a style attribute into my page's body tag.
body style="overflow: auto; position: static; touch-action: auto;"
In the CSP report, Chrome and Edge browsers suggested that I use style-src 'self' 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=';
and that does work to avoid that violation. Also, I am depending on ngx-image-cropper. That package inserts some inline styling as well. That styling seems to change with each page request. So, it doesn't seem possible to use a hash for them. Hopefully, Angular 14 will work better for me. Thanks again.

rickz21 avatar Apr 19 '22 16:04 rickz21

I read https://github.com/angular/angular/issues/6361 https://github.com/angular/components/issues/24633
and see that there are different opinions.

rickz21 avatar Apr 19 '22 18:04 rickz21

https://0xdbe.github.io/AngularSecurity-DisableInlineCriticalCSS/

jdavidhermoso avatar May 05 '22 15:05 jdavidhermoso

@jdavidhermoso I tried that configuration. It didn't stop Angular from adding inline styling to my body tag.

rickz21 avatar May 05 '22 19:05 rickz21

Hey @rickz21 the style tag on the body isn't related to this issue. This one is about angular automatically inlining critical css. It sounds like your styles on the body could be coming from something else. Maybe it's another dependency? Might be worth posting a new issue if you can create minimal reproduction

jpduckwo avatar May 05 '22 23:05 jpduckwo

@jpduckwo Angular is the only dependency. I will do as you suggest. I will try to create a minimal demonstration.

rickz21 avatar May 06 '22 03:05 rickz21

Hello All,

In my case, my project uses Angular 13 latest; and this issue was happening only on PROD build. Not on Dev/Staging builds. I searched angular documentation; no where it is mentioned why only production grade build gets affected?

Any clarity would enlighten me on this.

Thanks

ZenwalkerD avatar May 13 '22 04:05 ZenwalkerD

@ZenwalkerD check your angular.json, your settings for dev/staging will have a different optimization setting than prod. That's my guess :)

jpduckwo avatar May 13 '22 05:05 jpduckwo

Thank's @tiberiuzuld :tada:

I used your solution https://github.com/angular/angular-cli/issues/20864#issuecomment-844823912 for a Chrome extension created with [email protected]. I also needed the code generated by the ng build command to have no inline script.

I put the following optimization configuration in my angular.json file:

          "configurations": {
            "production": {
              "...": "...",
              "optimization": {
                "scripts": true,
                "styles": {
                  "minify": true,
                  "inlineCritical": false
                },
                "fonts": true
              }
            },
            "development": {
              "...": "..."
            }
          },

jprivet-dev avatar Jun 24 '22 17:06 jprivet-dev