Skip to content

autoCsp drops the integrity attribute from entry scripts when subresourceIntegrity is enabled #33844

Description

@Sese-Schneider

Command

build

Description

When subresourceIntegrity and security.autoCsp are both enabled, the entry scripts lose their SRI protection.

With only subresourceIntegrity: true, the index.html contains:

<script src="polyfills-ABC.js" type="module" crossorigin="anonymous" integrity="sha384-..."></script>
<script src="main-XYZ.js" type="module" crossorigin="anonymous" integrity="sha384-..."></script>

When autoCsp is also enabled, these tags get replaced by the generated loader script. The loader only carries over src, type, async and defer:

<script>
  var scripts = [['polyfills-ABC.js', 'module', false, false],['main-XYZ.js', 'module', false, false]];
  scripts.forEach(function(scriptUrl) {
    var s = document.createElement('script');
    s.src = scriptUrl[0];
    s.type = scriptUrl[1];
    s.async = !!scriptUrl[2];
    s.defer = !!scriptUrl[3];
    document.lastElementChild.appendChild(s);
  });
</script>

So integrity and crossorigin are gone and the browser no longer verifies the two entry bundles. The lazy chunks are fine, because their hashes live in the inline importmap, which autoCsp hashes into the policy and leaves as is. The entry scripts are the only gap.

There is no warning about this at build time. Users who enable both options probably expect them to work together.

The cause is createLoaderScript in packages/angular/build/src/utils/index-file/auto-csp.ts: the tuples collected in the startTag handler don't include the integrity and crossorigin attributes.

Reproduced with @angular/build 22.1.2.

Describe the solution you'd like

Extend the loader tuples with the two attributes. Dynamically created script elements support both properties, so this should be all that's needed:

var scripts = [['polyfills-ABC.js', 'module', false, false, 'sha384-...', 'anonymous'], ...];
scripts.forEach(function (scriptUrl) {
  var s = document.createElement('script');
  s.src = scriptUrl[0];
  s.type = scriptUrl[1];
  s.async = !!scriptUrl[2];
  s.defer = !!scriptUrl[3];
  if (scriptUrl[4]) { s.integrity = scriptUrl[4]; }
  if (scriptUrl[5]) { s.crossOrigin = scriptUrl[5]; }
  document.lastElementChild.appendChild(s);
});

The loader is hashed into the CSP after it is generated, so the hash stays correct.

Describe alternatives you've considered

Post-processing the built index.html to patch the loader and recompute its hash in the meta tag. Works, but depends on the internals of the generated loader, which can change between Angular versions.

Keeping the plain script tags for entry scripts is not an option, since 'strict-dynamic' blocks parser-inserted script tags regardless of host allowlists. That is the reason the loader exists in the first place.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions