Skip to content

Commit bc83cd2

Browse files
authored
Merge pull request #10593 from magento-cia/AC-16041-updated
SRI Functionality Review and Adjustment
2 parents 202fca8 + c2e37a2 commit bc83cd2

63 files changed

Lines changed: 10714 additions & 590 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
/**
4+
* Copyright 2026 Adobe
5+
* All Rights Reserved.
6+
*/
7+
-->
8+
<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
9+
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd">
10+
11+
<!--
12+
* Regression guard for AC-16636: sri.js race condition.
13+
*
14+
* sri.js registers a require.config({ onNodeCreated }) callback that stamps
15+
* integrity attributes onto every script tag RequireJS injects. It must be
16+
* evaluated by the browser BEFORE requirejs-config.js hands RequireJS its
17+
* module map, otherwise modules begin loading before the handler is wired up
18+
* and are injected without integrity attributes.
19+
*
20+
* This test:
21+
* 1. Deploys static content in production mode with JS minification enabled.
22+
* 2. Places a guest order to exercise the full checkout JS stack.
23+
* 3. Verifies on the order success page that sri.js appears before
24+
* requirejs-config.js in the DOM.
25+
* 4. Verifies that all versioned static JS files end in .min.js and carry
26+
* a valid sha256 integrity attribute.
27+
-->
28+
<test name="SriJsOrderingTest">
29+
<annotations>
30+
<features value="Checkout"/>
31+
<stories value="Subresource Integrity"/>
32+
<title value="Verify sri.js is positioned before requirejs-config.js and all minified scripts have SRI on checkout success page"/>
33+
<description value="AC-16636 regression guard: sri.js must appear before requirejs-config.js in the DOM so that the onNodeCreated SRI handler is registered before RequireJS starts loading modules. Verified on the order success page after a full guest checkout."/>
34+
<severity value="CRITICAL"/>
35+
<testCaseId value="AC-16636"/>
36+
<group value="csp"/>
37+
<group value="csp_sri"/>
38+
<group value="checkout"/>
39+
</annotations>
40+
41+
<before>
42+
<!-- Clear any stale maintenance flag left by a previous crashed test run -->
43+
<magentoCLI command="maintenance:disable" stepKey="disableStaleMaintenanceMode"/>
44+
<createData entity="SimpleProduct2" stepKey="createProduct"/>
45+
<magentoCLI command="config:set dev/js/minify_files 1" stepKey="enableMinification"/>
46+
<magentoCLI command="deploy:mode:set production" stepKey="setProductionMode"/>
47+
<magentoCLI command="setup:static-content:deploy en_US -s quick -f" stepKey="deployStaticContent" timeout="600"/>
48+
<magentoCLI command="maintenance:disable" stepKey="disableMaintenanceMode"/>
49+
<magentoCLI command="cache:flush" stepKey="flushCache"/>
50+
</before>
51+
52+
<after>
53+
<magentoCLI command="maintenance:disable" stepKey="disableMaintenanceAfter"/>
54+
<magentoCLI command="config:set dev/js/minify_files 0" stepKey="disableMinification"/>
55+
<magentoCLI command="deploy:mode:set developer" stepKey="restoreDeveloperMode"/>
56+
<magentoCLI command="cache:flush" stepKey="flushCacheAfter"/>
57+
<deleteData createDataKey="createProduct" stepKey="deleteProduct"/>
58+
</after>
59+
60+
<!-- Add product to cart and navigate to checkout -->
61+
<amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/>
62+
<waitForPageLoad stepKey="waitForProductPage"/>
63+
<waitForElementClickable selector="{{StorefrontProductActionSection.addToCart}}" stepKey="waitForAddToCartButton"/>
64+
<actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCart">
65+
<argument name="productName" value="$$createProduct.name$$"/>
66+
</actionGroup>
67+
<actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckout"/>
68+
<waitForPageLoad stepKey="waitForCheckoutLoad"/>
69+
<waitForElementVisible selector="{{CheckoutShippingSection.shippingTab}}" stepKey="waitForShippingTab"/>
70+
71+
<!--
72+
* Run SRI assertions here — on the checkout shipping page — where RequireJS
73+
* has fully bootstrapped and dynamically loaded all its modules.
74+
* This is the page most likely to expose the race condition.
75+
-->
76+
77+
<!--
78+
* Assert 1: sri.js appears before requirejs-config.js in the DOM.
79+
* This is the direct regression guard for the race condition fix in
80+
* Magento\RequireJs\Block\Html\Head\Config::_prepareLayout().
81+
-->
82+
<executeJS
83+
function="
84+
var scripts = Array.from(document.querySelectorAll('script[src]'));
85+
var sriIdx = scripts.findIndex(function(s) { return s.src.indexOf('Magento_Csp/js/sri') !== -1; });
86+
var configIdx = scripts.findIndex(function(s) { return s.src.indexOf('requirejs-config') !== -1; });
87+
return sriIdx !== -1 &amp;&amp; configIdx !== -1 &amp;&amp; sriIdx &lt; configIdx;
88+
"
89+
stepKey="checkSriJsBeforeRequireJsConfig"/>
90+
<assertTrue stepKey="assertSriJsBeforeRequireJsConfig"
91+
message="sri.js must appear before requirejs-config.js in the page head so that the onNodeCreated handler is registered before RequireJS starts loading modules">
92+
<actualResult type="variable">checkSriJsBeforeRequireJsConfig</actualResult>
93+
</assertTrue>
94+
95+
<!--
96+
* Assert 2: window.sriHashes inline script appears before requirejs-config.js in the DOM.
97+
* window.sriHashes must be defined before requirejs-config.js executes so that
98+
* sri.js onNodeCreated callbacks can read it on the first require() call.
99+
* On a cached load requirejs-config.js executes instantly — if window.sriHashes
100+
* is defined later in the page the integrity attributes are never applied.
101+
-->
102+
<executeJS
103+
function="
104+
var scripts = Array.from(document.scripts);
105+
var sriHashesIdx = scripts.findIndex(function(s) { return s.textContent.indexOf('window.sriHashes') !== -1; });
106+
var configIdx = scripts.findIndex(function(s) { var src = s.getAttribute('src') || ''; return src.indexOf('requirejs-config') !== -1; });
107+
return sriHashesIdx !== -1 &amp;&amp; configIdx !== -1 &amp;&amp; sriHashesIdx &lt; configIdx;
108+
"
109+
stepKey="checkSriHashesBeforeRequireJsConfig"/>
110+
<assertTrue stepKey="assertSriHashesBeforeRequireJsConfig"
111+
message="window.sriHashes inline script must appear before requirejs-config.js so it is defined before RequireJS fires onNodeCreated">
112+
<actualResult type="variable">checkSriHashesBeforeRequireJsConfig</actualResult>
113+
</assertTrue>
114+
115+
<!--
116+
* Assert 3: window.sriHashes is populated.
117+
* A count of zero means SRI is effectively disabled regardless of script ordering.
118+
-->
119+
<executeJS function="return window.sriHashes ? Object.keys(window.sriHashes).length : 0;" stepKey="getSriHashCount"/>
120+
<assertGreaterThan stepKey="assertSriHashesNotEmpty"
121+
message="window.sriHashes must be populated after static deploy">
122+
<actualResult type="variable">getSriHashCount</actualResult>
123+
<expectedResult type="int">0</expectedResult>
124+
</assertGreaterThan>
125+
126+
<!--
127+
* Assert 4: All versioned static scripts are minified (.min.js) and have an integrity attribute.
128+
* Minification is enabled in before, so every deployed JS file must end in .min.js.
129+
* Returns the count of scripts that are either non-minified or missing integrity.
130+
-->
131+
<executeJS
132+
function="
133+
var staticUrlPattern = /\/static\/version\d+\//;
134+
var violations = 0;
135+
document.querySelectorAll('script[src]').forEach(function(script) {
136+
var src = script.getAttribute('src');
137+
if (!src || !staticUrlPattern.test(src)) { return; }
138+
if (src.indexOf('.min.js') === -1) { violations++; }
139+
if (!script.getAttribute('integrity')) { violations++; }
140+
});
141+
return violations;
142+
"
143+
stepKey="getMinificationAndSriViolations"/>
144+
<assertEquals stepKey="assertAllScriptsMinifiedWithSri"
145+
message="All versioned static JS files must end in .min.js and have a sha256 integrity attribute">
146+
<actualResult type="variable">getMinificationAndSriViolations</actualResult>
147+
<expectedResult type="int">0</expectedResult>
148+
</assertEquals>
149+
150+
<!-- Complete the guest order to confirm checkout remains functional after the fix -->
151+
<actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="fillShipping"/>
152+
<waitForPageLoad stepKey="waitForPaymentPage"/>
153+
<waitForElementVisible selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrder"/>
154+
<waitForElementClickable selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="waitForPlaceOrderClickable"/>
155+
<click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/>
156+
<waitForPageLoad stepKey="waitForSuccessPage"/>
157+
<waitForElementVisible selector="{{CheckoutSuccessMainSection.success}}" stepKey="waitForSuccessMessage"/>
158+
159+
</test>
160+
161+
</tests>

app/code/Magento/Csp/Block/Sri/Hashes.php

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,13 @@
77

88
namespace Magento\Csp\Block\Sri;
99

10-
use Magento\Framework\UrlInterface;
11-
use Magento\Deploy\Package\Package;
1210
use Magento\Framework\App\ObjectManager;
1311
use Magento\Framework\View\Element\Template;
14-
use Magento\Framework\Exception\LocalizedException;
1512
use Magento\Framework\Serialize\SerializerInterface;
1613
use Magento\Framework\View\Element\Template\Context;
1714
use Magento\Csp\Model\SubresourceIntegrityRepositoryPool;
15+
use Magento\Csp\Model\SubresourceIntegrity\HashResolver\HashResolverInterface;
16+
use Psr\Log\LoggerInterface;
1817

1918
/**
2019
* Block for Subresource Integrity hashes rendering.
@@ -30,20 +29,36 @@ class Hashes extends Template
3029

3130
/**
3231
* @var SubresourceIntegrityRepositoryPool
32+
* @deprecated
33+
* @see HashResolverInterface - SRI hashes are now retrieved directly from the resolver
3334
*/
3435
private SubresourceIntegrityRepositoryPool $integrityRepositoryPool;
3536

37+
/**
38+
* @var HashResolverInterface|null
39+
*/
40+
private ?HashResolverInterface $hashResolver;
41+
42+
/**
43+
* @var LoggerInterface|null
44+
*/
45+
private ?LoggerInterface $logger;
46+
3647
/**
3748
* @param Context $context
3849
* @param array $data
3950
* @param SubresourceIntegrityRepositoryPool|null $integrityRepositoryPool
4051
* @param SerializerInterface|null $serializer
52+
* @param HashResolverInterface|null $hashResolver
53+
* @param LoggerInterface|null $logger
4154
*/
4255
public function __construct(
4356
Context $context,
4457
array $data = [],
4558
?SubresourceIntegrityRepositoryPool $integrityRepositoryPool = null,
46-
?SerializerInterface $serializer = null
59+
?SerializerInterface $serializer = null,
60+
?HashResolverInterface $hashResolver = null,
61+
?LoggerInterface $logger = null
4762
) {
4863
parent::__construct($context, $data);
4964

@@ -52,43 +67,30 @@ public function __construct(
5267

5368
$this->serializer = $serializer ?: ObjectManager::getInstance()
5469
->get(SerializerInterface::class);
70+
71+
$this->hashResolver = $hashResolver ?: ObjectManager::getInstance()
72+
->get(HashResolverInterface::class);
73+
74+
$this->logger = $logger ?? ObjectManager::getInstance()
75+
->get(LoggerInterface::class);
5576
}
5677

5778
/**
5879
* Retrieves integrity hashes in serialized format.
5980
*
60-
* @throws LocalizedException
61-
*
6281
* @return string
6382
*/
6483
public function getSerialized(): string
6584
{
66-
$result = [];
67-
68-
$baseUrl = $this->_urlBuilder->getBaseUrl(
69-
["_type" => UrlInterface::URL_TYPE_STATIC]
70-
);
71-
72-
$integrityRepository = $this->integrityRepositoryPool->get(
73-
Package::BASE_AREA
74-
);
75-
76-
foreach ($integrityRepository->getAll() as $integrity) {
77-
$url = $baseUrl . $integrity->getPath();
78-
79-
$result[$url] = $integrity->getHash();
85+
try {
86+
return $this->serializer->serialize($this->hashResolver->getAllHashes());
87+
} catch (\Exception $e) {
88+
// Return empty object on failure - checkout works without SRI
89+
$this->logger->warning(
90+
'SRI: Failed to retrieve hashes',
91+
['exception' => $e->getMessage()]
92+
);
93+
return '{}';
8094
}
81-
82-
$integrityRepository = $this->integrityRepositoryPool->get(
83-
$this->_appState->getAreaCode()
84-
);
85-
86-
foreach ($integrityRepository->getAll() as $integrity) {
87-
$url = $baseUrl . $integrity->getPath();
88-
89-
$result[$url] = $integrity->getHash();
90-
}
91-
92-
return $this->serializer->serialize($result);
9395
}
9496
}

app/code/Magento/Csp/Model/Deploy/Package/Processor/PostProcessor/Integrity.php

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22
/**
3-
* Copyright 2025 Adobe
3+
* Copyright 2026 Adobe
44
* All Rights Reserved.
55
*/
66
declare(strict_types=1);
@@ -17,6 +17,7 @@
1717
use Magento\Csp\Model\SubresourceIntegrity\HashGenerator;
1818
use Magento\Framework\App\ObjectManager;
1919
use Psr\Log\LoggerInterface;
20+
use Magento\Framework\View\Asset\Minification;
2021

2122
/**
2223
* Post-processor that generates integrity hashes after static content package deployed.
@@ -53,21 +54,28 @@ class Integrity implements ProcessorInterface
5354
*/
5455
private LoggerInterface $logger;
5556

57+
/**
58+
* @var Minification
59+
*/
60+
private Minification $minification;
61+
5662
/**
5763
* @param Filesystem $filesystem
5864
* @param HashGenerator $hashGenerator
5965
* @param SubresourceIntegrityFactory $integrityFactory
6066
* @param SubresourceIntegrityCollector $integrityCollector
6167
* @param LoggerInterface|null $logger
6268
* @param SubresourceIntegrityRepositoryPool|null $repositoryPool
69+
* @param Minification|null $minification
6370
*/
6471
public function __construct(
6572
Filesystem $filesystem,
6673
HashGenerator $hashGenerator,
6774
SubresourceIntegrityFactory $integrityFactory,
6875
SubresourceIntegrityCollector $integrityCollector,
6976
?LoggerInterface $logger = null,
70-
?SubresourceIntegrityRepositoryPool $repositoryPool = null
77+
?SubresourceIntegrityRepositoryPool $repositoryPool = null,
78+
?Minification $minification = null
7179
) {
7280
$this->filesystem = $filesystem;
7381
$this->hashGenerator = $hashGenerator;
@@ -76,6 +84,8 @@ public function __construct(
7684
$this->logger = $logger ?? ObjectManager::getInstance()->get(LoggerInterface::class);
7785
$this->repositoryPool = $repositoryPool ??
7886
ObjectManager::getInstance()->get(SubresourceIntegrityRepositoryPool::class);
87+
$this->minification = $minification ??
88+
ObjectManager::getInstance()->get(Minification::class);
7989
}
8090

8191
/**
@@ -84,35 +94,46 @@ public function __construct(
8494
public function process(Package $package, array $options): bool
8595
{
8696
$staticDir = $this->filesystem->getDirectoryRead(
87-
DirectoryList::ROOT
97+
DirectoryList::STATIC_VIEW
8898
);
8999

90100
foreach ($package->getFiles() as $file) {
91101
if (strtolower($file->getExtension()) === "js") {
92-
$integrity = $this->integrityFactory->create(
93-
[
94-
"data" => [
95-
'hash' => $this->hashGenerator->generate(
96-
$staticDir->readFile($file->getSourcePath())
97-
),
98-
'path' => $file->getDeployedFilePath()
102+
try {
103+
$deployedFilePath = $this->minification->addMinifiedSign(
104+
$file->getDeployedFilePath()
105+
);
106+
$fileContent = $staticDir->readFile($deployedFilePath);
107+
108+
$integrity = $this->integrityFactory->create(
109+
[
110+
"data" => [
111+
'hash' => $this->hashGenerator->generate($fileContent),
112+
'path' => $deployedFilePath
113+
]
99114
]
100-
]
101-
);
115+
);
102116

103-
$this->integrityCollector->collect($integrity);
117+
$this->integrityCollector->collect($integrity);
118+
} catch (\Exception $e) {
119+
// Continue processing other files if this one fails
120+
$this->logger->warning(
121+
'Integrity PostProcessor: ' . $e->getMessage()
122+
);
123+
}
104124
}
105125
}
106126

107127
// Save collected data directly to repository before process exits
108128
$collectedData = $this->integrityCollector->release();
109129
if (!empty($collectedData)) {
110-
$area = explode('/', $package->getPath())[0];
130+
$context = $package->getPath();
111131
try {
112-
$this->repositoryPool->get($area)->saveBunch($collectedData);
132+
$this->repositoryPool->get($context)->saveBunch($collectedData);
113133
} catch (\Exception $e) {
114-
//phpcs:ignore
115-
$this->logger->error('Integrity PostProcessor: Failed saving to ' . $area . ' repository: ' . $e->getMessage());
134+
$this->logger->error(
135+
'Integrity PostProcessor: Failed saving to ' . $context . ' repository: ' . $e->getMessage()
136+
);
116137
}
117138

118139
// Clear collector for next package (if any)

0 commit comments

Comments
 (0)