When an XSS attack successfully injects JavaScript into a web page, the issue is not just a change in appearance. The script can attempt to read data on the page, alter forms, redirect visitors to fake sites, or perform actions on behalf of logged-in users.
Input validation and escaping remain the primary defenses. However, websites also need an additional layer if there are overlooked vulnerabilities. This is where Content Security Policy or CSP comes in: a policy sent from the server to the browser to determine which scripts, images, frames, and connections the page is allowed to use.
CSP is Not a Replacement for Secure Coding
CSP should be understood as an emergency brake, not a substitute for a well-maintained engine. If the application code still outputs user input without escaping, the XSS vulnerability must be fixed at its source. CSP helps limit the impact when an unauthorized script attempts to enter or run.
For example, a page should only run scripts that are given a nonce. A nonce is a random value generated by the server for each response. The browser only executes <script> tags that have a nonce matching the value in the CSP header.
Why Allowlisting is Often Not Enough
The old approach typically involves creating a list of trusted domains, such as allowing scripts from the own domain and a few third-party services. The problem is that this list can easily become too permissive. A single trusted domain can load additional scripts, switch endpoints, or have configurations that are not entirely under our control.
A stricter approach uses nonce or hash to specify which particular scripts are allowed to run. Both OWASP and MDN explain nonce-based or hash-based CSP as a stronger pattern compared to simply adding many domains to the allowlist.
Choosing Nonce or Hash
Nonce for Dynamic Pages
Nonce is suitable if the server generates pages dynamically. The value must be random, hard to guess, and regenerated for each response.
Content-Security-Policy: script-src 'nonce-RANDOM_VALUE' 'strict-dynamic'; object-src 'none'; base-uri 'none'The script tags created by the application then use the same value:
<script nonce="RANDOM_VALUE" src="/assets/app.js"></script>The important thing is not just to add the nonce attribute to all script tags. Nonce should only be given to scripts that are indeed trusted. If the system automatically adds nonce to all tags, scripts injected by attackers could also gain permission.
Hash for Static Scripts
Hash is more practical for inline scripts that rarely change. The browser computes the hash of the script's content and only executes it if the result matches the hash in the CSP policy.
Content-Security-Policy: script-src 'sha256-HASH_SCRIPT'; object-src 'none'; base-uri 'none'The downside is that small changes—including spaces or formatting—can alter the hash. Therefore, the hash needs to be updated every time the script content changes.
Start with Report-Only Mode
The most common mistake when implementing CSP is to immediately enable a strict policy on the production website. As a result, payment features, analytics, comment widgets, or page editors suddenly stop working.
Use the Content-Security-Policy-Report-Only header first. In this mode, the browser logs violations but does not block resources. Check the reports in Developer Tools, then note which scripts are truly needed.
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-RANDOM_VALUE'; object-src 'none'; base-uri 'none'Once the list of dependencies is understood and important errors are resolved, the policy can then be moved to the Content-Security-Policy header.
Practical Steps for PHP Websites
- Inventory scripts. List internal scripts, CDNs, analytics, chat widgets, payment systems, and other integrations loaded on the page.
- Reduce inline JavaScript. Move code to external files whenever possible. This makes rules easier to maintain and test.
- Create nonce with a secure generator. In PHP, use functions like
random_bytes(), not regular random numbers or timestamps. - Send nonce through templates. The same value must be used in the header and on the allowed script tags.
- Test with Report-Only. Open important pages: login, checkout, forms, dashboards, and pages with third-party widgets.
- Enable gradually. Start with the easiest-to-control pages before applying the policy site-wide.
A simple example in PHP:
<?php
$nonce = base64_encode(random_bytes(16));
header("Content-Security-Policy: script-src 'nonce-$nonce'; object-src 'none'; base-uri 'none'");
?>
<script nonce="<?php echo htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8'); ?>">
initializeApp();
</script>This example still needs to be adjusted according to the application architecture, caching, and third-party scripts used. If the HTML page is cached as a static file, applying a nonce per response becomes more challenging, and a hash approach may be more suitable.
What About WordPress?
WordPress and plugins can add scripts from many sources. Therefore, applying CSP globally without an audit often leads to conflicts. Start with the most important pages, check violation reports, and evaluate plugins that inject inline scripts or call external domains.
Do not resolve all errors by adding 'unsafe-inline' or allowlisting as many domains as possible. Both can indeed make features work again, but they also reduce the benefits of CSP protection.
If WordPress connects to external systems via REST API, use integration-specific credentials, such as Application Passwords, instead of the main user password. These credentials can be created per application and revoked without changing the main password.
What Does This Mean for Us?
CSP is most useful when treated as part of the development process, not as a security accessory that is installed once and forgotten. Every new plugin, tag manager, widget, and frontend change can alter policy needs.
A brief checklist that can be done now:
- Check if the website has a CSP header.
- Test important pages with Report-Only mode.
- Identify inline scripts that are still in use.
- Remove unnecessary third-party dependencies.
- Ensure input remains escaped and database queries continue to use prepared statements.
- Document the reasons for each domain included in the policy.
The ultimate goal is not to create the longest header but to establish rules that are strict enough to limit unknown scripts and well-tested enough to keep the website functioning. With a gradual approach, CSP can become a sensible layer of defense—not a new source of problems.
Sources & Further Reading
- Content Security Policy Cheat Sheet - OWASP
- Content Security Policy (CSP) - MDN Web Docs
- Content Security Policy implementation - MDN Web Docs
- Authentication - WordPress REST API Handbook
- Application Passwords - WordPress Advanced Administration Handbook
– Rio Yotto @rioyotto
