Problem/Motivation

Implement SRI for aggregated JS libraries

Steps to reproduce

Install Drupal, enable JS aggregation and inspect a page - see no SRI in place

Proposed resolution

Implement SRI for aggregated JS libraries

Remaining tasks

Make it configurable

User interface changes

none

API changes

none

Data model changes

none

Release notes snippet

Issue fork drupal-3304450

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

Emil Stoianov created an issue. See original summary.

wim leers’s picture

You've got my attention! 👀

nod_’s picture

nod_’s picture

Issue summary: View changes
emil stoianov’s picture

Issue summary: View changes
nod_’s picture

should that be done for CSS as well?

nod_’s picture

Title: Implement SRI for aggregated JS libraries » Implement SRI for aggregated assets
emil stoianov’s picture

StatusFileSize
new1.1 KB

nod_ You are right It might be done for all assets.
It is limited to JS currently as it is a bigger safety concern.

I guess I should upload a patch file also so it could be included in composer.json for those who wish to have it right away

nod_’s picture

Status: Needs review » Needs work

crossorigin feels like scope creep though, and could be added in the library definition directly (or by default if we wanted to) (not for aggregated assets) I feel like the crossorigin config should be handled by modules such as CDN that allows files to be served from a different domain.

emil stoianov’s picture

Status: Needs work » Needs review

Unfortunately any arguments added to the library will force it to be pushed out of the aggregation group thus standing on it own and not be part of the aggregated file.
The effort of the patch is to have the aggregated files to have the SHA hash added.

Never questioned the need of crossorigin="anonymous" part from the example https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity here

Could you please explain what are the implications of that for aggregated JS files served from CDN?
It seems that crossorigin="same-origin" is the best for the scripts coming from drupal/contrib/theme libraries

Having in mind that the other libraries might be external - those never make it to the aggregation file so I am not sure we should cover those. So for drupal/contrib/theme libraries (the one which make it in the aggregation file) crossorigin="same-origin" is the best candidate.

What do you think?

emil stoianov’s picture

StatusFileSize
new1.11 KB

Updated the crossorigin="anonymous" to crossorigin="same-origin"

nod_’s picture

Version: 9.3.x-dev » 10.1.x-dev
Status: Needs review » Needs work
Issue tags: +Needs tests

all right so crossorigin attribute is required when using SRI per: https://w3c.github.io/webappsec-subresource-integrity/#cross-origin-data...

The appropriate values for the attribute are "", "anonymous", "use-credentials": https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin

So the patch in #9 which sets "anonymous" is correct and adding this attribute is not scope creep.

Putting as NW because we need to handle the CSS as well, and we'll need a couple of tests as well.

emil stoianov’s picture

Version: 10.1.x-dev » 9.3.x-dev
Status: Needs work » Needs review
StatusFileSize
new3.93 KB

This one handles existing aggregated files and now covers aggregated JS and CSS files

wim leers’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php
    @@ -160,6 +163,28 @@ public function optimize(array $css_assets) {
    +  protected function addSriArguments(array &$css_assets, string $data) {
    

    🤓 Nits:

    1. I think "arguments" was intended to be "attributes" here?
    2. I think we can add the void return type?
  2. +++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php
    @@ -160,6 +163,28 @@ public function optimize(array $css_assets) {
    +      $hash = base64_encode(hash('sha384', $data, TRUE));
    

    🤔

    1. Why sha384 and not sha256 or sha512?
    2. What's the performance impact? Is it measurable at all? (A simple test using ab would suffice IMHO.)
  3. +++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php
    @@ -159,6 +162,28 @@ public function optimize(array $js_assets) {
    +  /**
    +   * Implement SRI for a given group of JS assets.
    +   *
    +   * @param array $js_assets
    +   *   A group of JS assets.
    +   * @param string $data
    +   *   Content of the asset file.
    +   */
    +  protected function addSriArguments(array &$js_assets, string $data) {
    +    if (!isset($js_assets['attributes']['integrity'])) {
    +      $hash = base64_encode(hash('sha384', $data, TRUE));
    +      $attributes = [
    +        'integrity' => 'sha384-' . $hash,
    +        'crossorigin' => 'anonymous',
    +      ];
    +      if (!isset($js_assets['attributes'])) {
    +        $js_assets['attributes'] = [];
    +      }
    +      $js_assets['attributes'] = array_merge($attributes, $js_assets['attributes']);
    +    }
    +  }
    +
    

    🙏 This is 100% the same. Can we move this into a small trait that both classes reuse?

ravi.shankar’s picture

StatusFileSize
new3.94 KB
new1.88 KB

Tried to address point number 1 and 2.1 of comment #15, keeping the status needs work for point 2.2 on comment #15.

emil stoianov’s picture

StatusFileSize
new3.95 KB

Thank you for your eyes on it, Wim!

1.1 Changed
1.2 Added
2.1 It is the example from https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
2.2 It makes sense to test if there is another solution to compare with
3. Most of the functions in this 2 files are the same - if someone wants to reorganize that - please feel free.

catch’s picture

Version: 9.3.x-dev » 11.x-dev

Needs a re-roll, change would have to happen in the *Lazy versions of those classes now.

wim leers’s picture

Issue tags: +Needs reroll
akashkumar07’s picture

Assigned: emil stoianov » Unassigned
StatusFileSize
new4.05 KB

Adding a reroll of #17.

akashkumar07’s picture

Status: Needs work » Needs review
wim leers’s picture

Status: Needs review » Needs work
Issue tags: -Needs reroll +needs profiling

Thanks, @AkashKumar07! 😊

Unfortunately, #18 has not yet been addressed. It's still happening only in the deprecated classes 😇

This also still needs test coverage and profiling (see #15).

narendrar’s picture

Trait added, Changes done in Lazy versions of classes also.
Also strongest hash used.
I am not sure about 2.2 and how to write test to validate it. Any reference will be helpful.
Thanks

narendrar’s picture

narendrar’s picture

Status: Needs work » Needs review
smustgrave’s picture

Status: Needs review » Needs work

Have not reviewed.

Moving to NW for the test coverage. But good to see all the current tests passed!

narendrar’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
StatusFileSize
new349.58 KB
new308.11 KB
new306.92 KB
new283.19 KB

Done Apache benchmark testing on local.

Screenshots attached.
On 11.x without SRI:

With Sha-256

With Sha-384

With Sha-512

catch’s picture

I don't think apache bench will give useful results here because the optimizer logic is cached per set of assets. Having said that sha384 and sha512 appear noticeably slower than HEAD and sha256 so if that's repeatable, then something might be getting picked up.

smustgrave’s picture

@catch should this be moved back to NW?

catch’s picture

@smustgrave no I think we need better benchmarking - I would probably have used microtime() style benchmarking for this so it's possible to compare only the hashing logic and not the entire request.

narendrar’s picture

StatusFileSize
new6.15 KB

Added microtime in patch for testing, and below are my findings:

sha512:
Logged-In, home page after cache clear:
CSS Execution Time: 0.00030803680419922 seconds
CSS Execution Time: 0.00040578842163086 seconds
JS Execution Time: 2.0980834960938E-5 seconds
JS Execution Time: 0.0010080337524414 seconds

Anonymous, home page after cache clear:
CSS Execution Time: 5.2928924560547E-5 seconds
CSS Execution Time: 0.00041604042053223 seconds
JS Execution Time: 0.00011801719665527 seconds

sha384
Logged-In, home page after cache clear:
CSS Execution Time: 0.00030779838562012 seconds
CSS Execution Time: 0.00040507316589355 seconds
JS Execution Time: 1.8119812011719E-5 seconds
JS Execution Time: 0.0010049343109131 seconds

Anonymous, home page after cache clear:
CSS Execution Time: 9.2029571533203E-5 seconds
CSS Execution Time: 0.00089097023010254 seconds
JS Execution Time: 0.00011897087097168 seconds

sha256:
Logged-In, home page after cache clear:
CSS Execution Time: 0.00047683715820312 seconds
CSS Execution Time: 0.00062894821166992 seconds
JS Execution Time: 2.0980834960938E-5 seconds
JS Execution Time: 0.0015959739685059 seconds

Anonymous, home page after cache clear:
CSS Execution Time: 7.2002410888672E-5 seconds
CSS Execution Time: 0.00062084197998047 seconds
JS Execution Time: 0.00017786026000977 seconds

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new3.04 KB

The Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

This does not mean that the patch needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

narendrar’s picture

Status: Needs work » Needs review

Test failure seems to be related to #31

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new3.03 KB

The Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

This does not mean that the patch needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

catch’s picture

I think 'seconds' in the patch should be 'microseconds'?

No that's wrong, it really is taking multiple seconds afaict, seems a bit much.

narendrar’s picture

Status: Needs work » Needs review
Issue tags: +no-needs-review-bot

Avoided test bot for now.

neclimdul’s picture

Status: Needs review » Needs work
Issue tags: +Needs tests

Skimming the discussion, I'm not sure which result was giving multiple seconds. Do you mean the _really_ small results that are in scientific notation with E-5?

I'm curious how this is able to generate hashes of files that are lazily generated. Some more digging to understand the patch I guess unless someone has a hint.

Marking NW and adding needs tests back because there are not tests in the patch. We can continue discussing performance impact outside that fact.

catch’s picture

Do you mean the _really_ small results that are in scientific notation with E-5?

Doh... yes, missed that the E-5...

larowlan’s picture

Also #15 asked for the method to be in a trait, I agree that feels reasonable even though its not the rule of 3

narendrar’s picture

Status: Needs work » Needs review
Issue tags: -no-needs-review-bot, -Needs tests
StatusFileSize
new6.87 KB
new993 bytes
wim leers’s picture

Status: Needs review » Needs work
Issue tags: +Security improvements, +Needs frontend framework manager review
  1. +++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php
    @@ -150,6 +152,8 @@ public function optimize(array $css_assets, array $libraries) {
    +              // Implement SRI.
    
    +++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php
    @@ -149,6 +151,8 @@ public function optimize(array $js_assets, array $libraries) {
    +              // Implement SRI.
    

    🤓 Nit: I think these comments can be omitted now?

  2. +++ b/core/tests/Drupal/FunctionalTests/Asset/AssetOptimizationTest.php
    @@ -337,4 +337,23 @@ protected function invalidExclude(string $url): string {
    +    $this->assertStringContainsString('integrity="sha512-', $page->getContent());
    

    Shouldn't we also test the correctness of the hash?

    If we get this wrong, the consequences could be pretty big? Browsers would REFUSE to execute the JS or apply the CSS! See https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integr....

    … or I guess that would mean all of our functional JS tests would fail? 🤔

    Can we make the SRI attribute intentionally incorrect for a moment to check whether that would indeed those tests to fail? 🙏

catch’s picture

+++ b/core/lib/Drupal/Core/Asset/AssetSetSriTrait.php
index ec3b805c971..006fe21e8ba 100644
--- a/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php

Not sure about adding this to the deprecated classes - I guess it doesn't hurt, but could also be left out.

catch’s picture

If we get this wrong, the consequences could be pretty big? Browsers would REFUSE to execute the JS or apply the CSS! See https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integr....

… or I guess that would mean all of our functional JS tests would fail?

I think we need to check what happens in this case:

#3397713: [Performance regression] Starting with Drupal 10.1, some sites hit PHP for every page view due to aggregated asset URL hash mismatch from different order of asset items

i.e. correct asset definitions, but a mis-matching hash, which is due to indeterminate asset order with the same sets of libraries in certain situations. Ideally we'd never, ever end up in that situation, but at the moment we do.

narendrar’s picture

Addressed #42 and #41 except

Can we make the SRI attribute intentionally incorrect for a moment to check whether that would indeed those tests to fail?
natefollmer’s picture

I've applied the patch from comment #44 and everything works fine when a user is logged in, but any aggregated css/js is blocked for anonymous users.

Failed to find a valid digest in the 'integrity' attribute for resource 'XXX' with computed SHA-512 integrity 'XXX'. The resource has been blocked.

prudloff changed the visibility of the branch 3304450-implement-sri-for to hidden.

plopesc made their first commit to this issue’s fork.

plopesc’s picture

I found this issue after SRI has been flagged as a need by a PCI compliance audit for one of our sites.

Checked the original approach and it does not apply anymore, as expected because the old deprecated classes have been already removed.

However, I feel that we are in a kind of rabbithole here.

Per #1014086: Stampedes and cold cache performance issues with css/js aggregation, the aggregated assets are not being generated on the fly for very good reasons. However, for SRI we need the actual assets to calculate the hash and include it in the original HTML markup.

Do you have in mind any possible approach that could satisfy both the needs of #1014086: Stampedes and cold cache performance issues with css/js aggregation and allow to implement SRI in a generic way for Drupal sites?

catch’s picture

https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresour... very specifically says that SRI is intended for files loaded from CDNs, are these audits really flagging it as a need for locally hosted assets? Seems contrary to the whole point since if your own server gets compromised then they can do a lot more than tamper with aggregated assets on the filesystem.

catch’s picture

Status: Needs work » Postponed (maintainer needs more info)

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.