Building a Content Security Policy (CSP) from Scratch
AI generated
OWASP
0x00
Security · Content-Security-Policy · XSS Protection · Magento 2
Building a Content Security Policy (CSP) from Scratch
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.

18 min. read CSP Directives · Nonce & Hash · Reporting API Magento 2.4.8 · Hyva Theme · OWASP

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.

11. FAQ: Content Security Policy (CSP)

1What is a Content Security Policy and what is it used for?
An HTTP header with an allowlist of permitted sources for scripts, styles, images, and other resources. The most effective line of defense against cross-site scripting, even when output encoding fails.
2What does the default-src directive do?
Fallback for any fetch directive not set explicitly. A restrictive default-src 'self' loads unspecified resource types only from the same domain automatically.
3Nonce-based vs. hash-based script allowlisting?
Nonce: fresh random value per request, for dynamic inline scripts. Hash: computed once from the script content, only suited for static, unchanging code.
4Why is unsafe-inline dangerous?
Allows every inline script regardless of origin. An injected script tag runs normally, meaning the CSP no longer protects against the most common attack vector.
5What does report-only mode do?
Logs violations without blocking anything. Allows a new policy to be tested against real traffic for days before it gets enforced.
6report-to vs. report-uri?
report-uri is legacy and sends directly to a URL. report-to uses the Reporting API with a named endpoint and delivers reports in batches in the new JSON format.
7Building a CSP for Magento/Hyva?
Magento_Csp with a custom PolicyCollectorInterface for dynamic sources, complemented by the hyva-themes/magento2-csp module for automatic nonce injection in templates.
8What is $hyvaCsp->registerInlineScript()?
A required call after every inline script tag in phtml files, so the Hyva CSP module can hash the content or attach the nonce. Without that call, the policy blocks the script.
9Common mistakes with checkout and Maps?
Missing connect-src entries break payment APIs, missing frame-src or script-src entries lead to empty Maps boxes and video embeds that never start.
10How do you test and audit a CSP?
Google CSP Evaluator for structural weaknesses, Chrome DevTools console for violation messages, automated Lighthouse check in the CI pipeline before every deployment.