from the first directive to a production-ready rollout
A misconfigured or completely missing Content Security Policy leaves the door wide open for cross site scripting attacks, even when the rest of the code is clean. This article walks through building directives like script src and frame ancestors correctly, using report only mode for a safe rollout, and hardening the policy specifically for Magento and Hyva storefronts.
Table of Contents
- 1. What a Content Security Policy actually does
- 2. CSP directives in detail: default-src, script-src & co.
- 3. Nonce-based vs. hash-based script allowlisting
- 4. unsafe-inline and unsafe-eval: the biggest holes in a CSP
- 5. Report-only mode: rolling out a CSP risk-free
- 6. Violation reporting: report-to and the Reporting API
- 7. Building a CSP for Magento and Hyva storefronts
- 8. Integrating third-party scripts into the CSP
- 9. CSP misconfigurations compared side by side
- 10. Summary
- 11. FAQ
1. What a Content Security Policy actually does
A Content Security Policy is an HTTP response header that explicitly tells the browser which sources are allowed to serve scripts, styles, images, and other resources. Unlike a web application firewall filter, a CSP does not work by pattern-matching traffic, it enforces an allowlist that the browser itself applies before a resource is even executed. That makes CSP the most effective line of defense against cross-site scripting: even if an attacker successfully injects HTML or JavaScript into a page, a correctly configured policy prevents the browser from executing that script at all, or from sending data to a foreign domain.
Building a CSP from scratch, however, is not a copy-paste exercise from a sample header. Every directive has to match the page's actual resource landscape, otherwise checkout, map integrations, or video embeds break. The following sections build a CSP systematically: from directive syntax through nonce- and hash-based allowlisting to a production-ready rollout with report-only mode and violation reporting, tailored specifically for Magento and Hyva storefronts.
2. CSP directives in detail: default-src, script-src & co.
Every CSP consists of a list of directives separated by semicolons, where each directive defines a resource type and a set of allowed sources. default-src 'self' is the fallback for any fetch directive that isn't set explicitly and should always serve as the most restrictive baseline. script-src controls where JavaScript may be loaded and executed from, style-src governs CSS, and img-src determines allowed image sources, where data: often needs to be allowed deliberately for data URIs, for example SVG icons or base64-encoded placeholder images.
connect-src is particularly relevant for modern storefronts because it controls AJAX calls, WebSockets, and fetch() requests, exactly the channels through which payment providers and tracking scripts exchange data. frame-ancestors replaces the legacy X-Frame-Options header and defines which pages are allowed to embed the page in an iframe, a central defense against clickjacking. Additional hardening directives like object-src 'none' and base-uri 'self' close off older attack vectors via plugins and base-tag injection respectively.
# .htaccess or vhost config: baseline CSP for a Magento storefront
<IfModule mod_headers.c>
Header always set Content-Security-Policy "default-src 'self'; \
script-src 'self' 'nonce-{NONCE}' https://www.googletagmanager.com; \
style-src 'self' 'unsafe-inline'; \
img-src 'self' data: https://www.google-analytics.com; \
connect-src 'self' https://www.google-analytics.com; \
frame-ancestors 'self'; \
object-src 'none'; \
base-uri 'self'; \
report-to csp-endpoint"
Header always set Reporting-Endpoints "csp-endpoint=\"https://mironsoft.de/csp/report\""
</IfModule>
3. Nonce-based vs. hash-based script allowlisting
Once script-src no longer allows 'unsafe-inline' across the board, every legitimate inline script needs an explicit pass. Two mechanisms handle that: nonce-based and hash-based allowlisting. With the nonce approach, the server generates a random, cryptographically secure value on every page load, embeds it both in the CSP header as 'nonce-XYZ' and as a nonce attribute on the relevant <script> tag. The browser only executes scripts whose nonce attribute exactly matches the one in the header, and an attacker cannot guess that value because it changes on every request.
Hash-based allowlisting suits static inline scripts whose content never changes: the SHA-256 hash of the exact script content is computed once and stored in the header as 'sha256-...'. The advantage over nonces is that no per-request server templating is needed; the downside is that a single changed character in the script, say from minification or a whitespace change, invalidates the hash and blocks the script.
<!-- Nonce-based: server generates a fresh random value per request -->
<script nonce="r4nd0mBase64Value==">
window.dataLayer = window.dataLayer || [];
</script>
<!-- Matching CSP header directive -->
<!-- script-src 'self' 'nonce-r4nd0mBase64Value==' -->
<!-- Hash-based: no nonce needed, but the script content must never change -->
<script>
console.log('static inline script, hashed at build time');
</script>
<!-- Matching CSP header directive, generated once via sha256sum -->
<!-- script-src 'self' 'sha256-B2yPHKaXnvFWtRChIbabYmUBFZdVfKKz1CvmYuXVUJI=' -->
4. unsafe-inline and unsafe-eval: the biggest holes in a CSP
'unsafe-inline' and 'unsafe-eval' are the two directive values that undermine a CSP most effectively, because they reopen exactly the attack surface CSP is meant to close. 'unsafe-inline' in script-src allows every inline script regardless of origin or content, so a <script> tag injected via XSS will run just fine, the policy no longer offers any protection at all against the most common real-world attack vector. Browsers automatically ignore 'unsafe-inline' once a nonce or hash is present in the same directive value, a useful fallback mechanism for older browsers, but that should not be confused with deliberately using both values together.
'unsafe-eval' allows eval(), new Function(), and similar dynamic code execution from strings, a classic way to turn injected text into executable code. Some older JavaScript libraries need 'unsafe-eval' internally, which is why an audit of every bundled third-party script before tightening the policy is essential.
5. Report-only mode: rolling out a CSP risk-free
Never roll out a new CSP directly in enforce mode on a production page. The Content-Security-Policy-Report-Only header behaves identically to the real policy but blocks nothing at all, it merely logs which violations would have occurred under an enforced policy. That makes it possible to observe real user traffic for days or weeks, including rare code paths like seasonal campaign pages, A/B test variants, or rarely used checkout steps, without a single customer ever seeing a broken page.
In practice, a staged approach works best: roll out a deliberately generous policy in report-only mode first, collect and review the incoming reports over several days, add unexpected but legitimate sources to the allowlist as needed, and only once the report rate has dropped to near zero, switch on the same header without the -Report-Only suffix. Both headers can also be set in parallel to test a stricter future policy alongside the one currently enforced.
# nginx: roll out a new CSP in report-only mode before enforcing it
server {
listen 443 ssl;
server_name mironsoft.de;
# Observe violations without blocking any resource yet
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'nonce-$request_id' https://www.googletagmanager.com; report-to csp-endpoint" always;
add_header Reporting-Endpoints 'csp-endpoint="https://mironsoft.de/csp/report"' always;
location /csp/report {
# Forward violation reports to the Magento CSP report controller
proxy_pass http://magento_upstream;
}
}
6. Violation reporting: report-to and the Reporting API
Without a configured report endpoint, CSP violations remain invisible, the browser blocks the resource but tells no one about it. The legacy report-uri directive sends violation reports in the old application/csp-report format directly to a given URL and is increasingly supported only as a fallback by modern browsers. The current standard is the Reporting API: the Reporting-Endpoints header defines a named endpoint, and the CSP directive report-to points to that name, violations are then collected and sent in batches in the new, structured JSON format instead of transmitting each individual violation immediately.
For Magento stores, a lightweight dedicated controller under a route like csp/report is recommended, one that validates incoming reports, deduplicates them, and forwards them to a monitoring system such as a log aggregator or Sentry. Without deduplication, a single blocked script that's included on every page can generate thousands of identical reports within minutes and overwhelm the endpoint.
{
"type": "csp-violation",
"url": "https://mironsoft.de/checkout/cart/",
"age": 42,
"body": {
"documentURL": "https://mironsoft.de/checkout/cart/",
"referrer": "https://mironsoft.de/",
"blockedURL": "https://evil-tracker.example/inject.js",
"effectiveDirective": "script-src-elem",
"originalPolicy": "default-src 'self'; script-src 'self' 'nonce-r4nd0mBase64Value' https://www.googletagmanager.com; report-to csp-endpoint",
"disposition": "enforce",
"statusCode": 200,
"sourceFile": "https://mironsoft.de/checkout/cart/",
"lineNumber": 128,
"columnNumber": 17
}
}
7. Building a CSP for Magento and Hyva storefronts
Magento already ships a complete CSP management framework with the Magento_Csp module, including admin configuration under Stores > Configuration > Security > Content Security Policy for simple directives. For more complex, dynamic requirements, you implement a custom PolicyCollectorInterface that adds additional sources at runtime, such as Google Tag Manager domains or payment provider hosts, to the relevant fetch policy, instead of maintaining header values statically in configuration.
On the Hyva side, the hyva-themes/magento2-csp module handles nonce injection for inline scripts automatically. Every .phtml file that outputs an inline <script> tag must call $hyvaCsp->registerInlineScript() immediately afterward, so the module can hash the script content or attach the current nonce and add it automatically to the composed policy. Forgetting that call risks either a script blocked by the policy or, worse, an Alpine.js setup that silently fails to initialize in enforce mode. This tight coupling between template and policy collector is the key difference from classic Magento themes, where inline scripts were usually passed through unchecked.
<?php
declare(strict_types=1);
namespace Mironsoft\Security\Csp;
use Magento\Csp\Api\PolicyCollectorInterface;
use Magento\Csp\Model\Policy\FetchPolicy;
use Magento\Framework\App\Area;
use Magento\Framework\App\State;
/**
* Adds required third party hosts to the storefront script-src directive.
*/
class ThirdPartyPolicyCollector implements PolicyCollectorInterface
{
/**
* @param State $appState Application state used to detect the current area.
*/
public function __construct(private readonly State $appState)
{
}
/**
* Extends the collected CSP policies with third party script sources.
*
* @param array $policies
* @param bool $isReportOnly
* @return array
*/
public function collect(array $policies, bool $isReportOnly = false): array
{
if ($this->appState->getAreaCode() !== Area::AREA_FRONTEND) {
return $policies;
}
// Allow Google Tag Manager and Google Analytics on storefront pages only
$policies[] = new FetchPolicy(
'script-src',
false,
['https://www.googletagmanager.com', 'https://www.google-analytics.com'],
[],
false,
false,
false,
[],
true
);
return $policies;
}
}
8. Integrating third-party scripts into the CSP
Virtually every Magento store embeds Google Analytics or Google Tag Manager, one or more payment providers, and a chat widget, and each of these systems needs its own entries across several directives at once. Google Tag Manager typically needs script-src https://www.googletagmanager.com and connect-src https://www.google-analytics.com, often supplemented with img-src for tracking pixels. Payment providers like Adyen, Braintree, or Klarna frequently load their own iframe-based hosted fields widget, which additionally requires frame-src entries for the relevant payment domain, not just script-src.
The safest approach is to check each third-party service's official CSP documentation instead of copying domains from the browser console by trial and error, since providers regularly change their subdomains and CDN hosts. Chat widgets like Zendesk or Intercom also frequently need worker-src for service-worker-based push notifications. A central list of all approved third-party domains, maintained as its own PolicyCollectorInterface per provider, keeps the policy maintainable instead of manually extending a monolithic header.
9. CSP misconfigurations compared side by side
The most common CSP misconfigurations don't come from a lack of knowledge about individual directives, but from the absence of a systematic testing routine before rollout. A production checkout that suddenly can't send payment data anymore, a Google Maps embed that shows up as an empty gray box, or a YouTube video embed that won't start are the three most visible symptoms of an overly restrictive policy, usually caused by missing frame-src or connect-src entries.
The table below compares typical misconfigurations with the recommended, secure alternative. For testing and auditing, the Google CSP Evaluator, the Chrome DevTools console, which prints a detailed message with the affected directive and blocked URL for every violation, and an automated Lighthouse check in the CI pipeline that flags a missing or overly permissive CSP as a security warning before a deployment ever goes live are all useful tools.
| Directive / scenario | Insecure / misconfigured | Recommended / secure | Why |
|---|---|---|---|
| script-src | 'unsafe-inline' |
'self' 'nonce-...' |
Prevents execution of injected inline scripts |
| object-src | not set | 'none' |
Closes plugin-based XSS vectors |
| frame-ancestors | missing entirely | 'self' |
Protects against clickjacking |
| base-uri | not set | 'self' |
Prevents base-tag injection |
| Violation reporting | no report-to configured | report-to + Reporting-Endpoints |
Violations become visible and auditable |
In practice, the same pattern keeps showing up: every one of the misconfigurations listed above can be avoided if a new policy is first tested against real traffic in report-only mode before it gets enforced. Repeating that process for every major feature release reliably prevents a tightened CSP from silently breaking checkout or a map integration.
Mironsoft
Web security, CSP audits, and Hyva hardening for Magento stores
Ready to build and roll out a Content Security Policy properly?
We analyze your existing resource landscape, build a tailored CSP with nonce-based script allowlisting, and roll it out risk-free via report-only mode, including violation reporting and Hyva-specific integration.
CSP audit
Analyzing the existing policy and missing headers, identifying unsafe-inline gaps
Rollout support
Reviewing the report-only phase, adding third-party domains, setting up a report endpoint
Hyva integration
Implementing PolicyCollectorInterface and embedding registerInlineScript() across every template
10. Summary
Building a Content Security Policy from scratch means deliberately deriving every directive from the page's actual resource landscape instead of copying a generic template. default-src 'self' as a restrictive baseline, deliberately extended script-src, style-src, img-src, and connect-src values, plus frame-ancestors against clickjacking, form the foundation. Nonce-based allowlisting replaces 'unsafe-inline' for dynamically rendered inline scripts, hash-based allowlisting suits static inline code that never changes.
The rollout determines success or failure: Content-Security-Policy-Report-Only collects real violation data over days before a policy gets enforced, report-to and the Reporting API provide structured, ongoing visibility into incoming violations. In Magento and Hyva stores, Magento_Csp and the registerInlineScript() pattern handle the technical implementation, and using these tools consistently protects checkout against cross-site scripting without breaking map integrations or payment widgets.
Building a Content Security Policy from Scratch: The Essentials at a Glance
Directives first
default-src 'self' as the baseline, refine script-src, style-src, img-src, connect-src, and frame-ancestors deliberately.
Nonce instead of unsafe-inline
A nonce generated per request replaces wildcard allowances, hash-based allowlisting for static inline code.
Report-only before enforce
Observe a new policy first, collect violations via report-to and the Reporting API, then enforce it.
Magento & Hyva
Magento_Csp PolicyCollectorInterface and $hyvaCsp->registerInlineScript() instead of manual header editing.