Skip to content

CSP & block_html Cache

If unsafe-inline is excluded from the script-src CSP policy, blocks with scripts cached in the block_html cache can cause issues.
This is also true for cached blocks where the inline script is rendered by a child block.

How the issue materializes depends on whether the page is cached in the full-page cache.

Uncached pages

On pages excluded from the full-page cache, a nonce attribute is injected into all registered <script> tags.
However, the value of the nonce attribute has to be different for every request.

On the request during which the block output is generated and written to the block_html cache, all works. The script tag contains the correct nonce.

However, on subsequent requests to the page, the cached record of the block will be used, but the nonce in the cached HTML will not match the nonce for the current request, so the script will not be executed by the browser.

Cached pages

On pages cached in the full-page cache, the SHAs of all registered scripts are added to the CSP HTTP header.

If a block is not stored in the block_html cache when the page is rendered, the script is registered, the SHA is added to the header, and everything works.

On subsequent requests, as long as the page is served from the full-page cache, the SHAs are returned together with the page content, and all is well.

However, if the block is already stored in the block_html cache before the cached page is rendered, the inline script will not be registered.
In this scenario, the SHA will be missing from the CSP header.

This scenario happens frequently when a cached block is used on both pages included and excluded from the full-page cache.

The only solution is to exclude scripts from the blocks cached in the block_html cache.
Excluding the whole block from the block_html cache is the fastest solution, however, it does mean the block content will be generated on every page request. If that is too resource-expensive, extracting the script from the cached block into a separate template block that isn't cached maintains the scalability benefits of the block_html cache while also resolving the problem.

What causes a block to be cached in the block_html cache?

A block with a cache_lifetime data record that is not false or null will be cached (where a value of 0 means the cache record will never expire).
Often the value is set with layout XML, but it can also be set in the block class or even in the template. For example

<block name="example" template="Magento_Theme::block-with-a-script.phtml">
    <arguments>
        <argument name="cache_lifetime" xsi:type="string">3600</argument>
    </arguments>
</block>

Excluding a block from the block_html cache on uncached pages

This can be done by setting the cache_lifetime data record to false.
This has to be done on the same level, where it is set in the first place (that is, layout XML, the block class, or the template).

To expand on the example from above, in a layout handle for the uncached page, the block cache could be deactivated with

<referenceBlock name="example">
    <arguments>
        <argument name="cache_lifetime" xsi:type="boolean">false</argument>
    </arguments>
</referenceBlock>

In PHP, determining if the page will be stored in the full page cache can be done via the layout instance using $this->layout->isCacheable().

The top-menu navigation block

One such cached block that is used on all pages is the top menu.
If you have custom uncached pages in your store, be sure to exclude it from the block_html cache on those routes using.

<referenceBlock name="topmenu_generic">
    <arguments>
        <argument name="cache_lifetime" xsi:type="boolean">false</argument>
    </arguments>
</referenceBlock>