PHP Architect logo

Want to check out an issue? Sign up to receive a special offer.

The Secret Header That Makes Your PHP App 10x More Secure

Posted by on November 15, 2025

 

 

As developers in the year 2025, keeping our systems and applications secure is of the utmost importance. One of the most common ways that attackers can make our applications less secure is by using a class of vulnerability called Cross-Site Scripting (XSS). XSS allows an attacker to inject malicious client-side scripts into our websites and have them run on our unsuspecting users’ computers. It might mine cryptocurrency, or it might try to steal their banking information.

This has been such a serious problem for so long that the W3C has created the Content Security Policy headers that can reduce, if not completely remove, the risk to our users.

In this article, we’ll discuss why having a CSP header is a must if you’re displaying any kind of dynamically generated content, how to add it to an existing website, and some important tips.

What is CSP?

I think the easiest way to grok CSP is to think of it as a whitelist for the browser. Instead of trying and failing to stop every possible bug server-side, you will use CSP to instruct the browser to only load scripts, styles, images, and other resources from specific places or with specific attributes. This will reduce the attack surface of your site because even if an attacker can inject script tags or inline scripts, CSP will minimize the harm they can do.

You do this by providing a header in your HTTP responses that will specify exactly what sources of external data are valid for your application. It can be sent either by your web server (more secure) or your code (more flexible). I like the approach of setting it via your web server because it’s harder for attackers to compromise this setting, but being able to set it in your code makes it easier to support situations where third-party libraries don’t “play nice” with your settings.

I should note that CSP is not a replacement for secure coding, but is another piece of our layered security approach.

Implementing CSP

Unless you’re on a greenfield application and have no users, I don’t recommend just switching CSP on and hoping for the best because something is going to stop working. Thankfully, CSP provides us a way to gradually turn it on.

Phase 1 – Report Only

Phase 1 of implementing CSP in your application is to start in report-only mode. In this mode, the browser will not block anything, but instead send violation reports to a destination you define. This helps you discover what your site actually uses and what will break when you enable the policy.

The HTTP header for this is “Content-Security-Policy-Report-Only”.

For example, let’s look at the following basic PHP file that we’ve named “test.php” with an example <script> tag that was injected by an attacker, but also includes a JS file we actually want to have loaded.

<html>
    Hi
    <!-- Injected -->
    <script>
        document.write("transfer all the bitcoin...");
    </script>
    ...

    <script src="/important.js"></script>
</html>

If we access this in the browser, we’ll get the injected bad code and also our good code:

Hi transfer all the bitcoin…
… do something helpful from important.js..

Collection Script

To start collecting data about what violations there are, we need a script that will store the responses to a log (or some other system) so we can then investigate it further.

At the root of your directory, create a file named “csp-violation-reports.php” and enter the following information:

<?php
$entityBody = file_get_contents("php://input");

file_put_contents("csp-violations.log", $entityBody);

This just takes the data sent to the script in the POST body and stores it into “csp-violations.log”.

Now we’re going to go back to our “test.php” file and add the following to the very top of the file.

<?php
header("Content-Security-Policy-Report-Only: script-src 'self';report-uri /csp-violation-reports.php");
?>

This is what’s going to tell the browser that we want to enable CSP in “report only” mode (“Content-Security-Policy-Report-Only”), that we only want JavaScript (“script-src”) to load using the same domain as the requested page (“‘self'”), and that if there are any violations to send a report to our script file (“report-uri /csp-violation-reports.php”).

Now, if we refresh our page, we’ll see it’s still running the injected code.

Hi transfer all the bitcoin…
… do something helpful from important.js..

But if we check our log file, we’ll see the following line.

{"csp-report":{"document-uri":"http://127.0.0.1:8080/test.php","referrer":"","violated-directive":"script-src-elem","effective-directive":"script-src-elem","original-policy":"script-src 'self';report-uri /csp-violation-reports.php","disposition":"report","blocked-uri":"inline","line-number":4,"source-file":"http://127.0.0.1:8080/test.php","status-code":200,"script-sample":""}}

There’s a lot here, but the important thing is that it’s telling us the “blocked-uri” was an “inline” script. You can then investigate how to fix the issue. This information will also be logged to the browser console, so you can see it as well.

For example, we might have a page that includes some JS from a CDN (which is the best way to run your site), and we need to allow that CDN. If we have the following on our page:

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>

The browser will report back the following which tells us the URI isn’t allowed.

{"csp-report":{"document-uri":"http://127.0.0.1:8080/test.php","referrer":"","violated-directive":"script-src-elem","effective-directive":"script-src-elem","original-policy":"script-src 'self';report-uri /csp-violation-reports.php","disposition":"report","blocked-uri":"https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js","status-code":200,"script-sample":""}}

And then we can add the specific domain to our “Content-Security-Policy-Report-Only” so it will be allowed, and we’ll no longer receive the report.

<?php
header("Content-Security-Policy-Report-Only: script-src 'self' https://cdn.jsdelivr.net;report-uri /csp-violation-reports.php");
?>

It’s generally a good idea to enable the reporting locally, do a bunch of testing to see what problems exist, and then either fix the generated HTML or make the rule more permissive before pushing to production, because the browser will make one HTTP request per violation, and depending on your traffic, it can really add up.

Phase 2 – Production Reporting

Once you feel comfortable, you’ve gotten most of the items cleaned up, you can push it out to your users. Again, we’re only reporting, so nothing should break.

Depending on the level of traffic your site gets, you might want to check back after an hour, a day, and a week and see what new items are reported back to your server and fix them so the errors are no longer reported. Repeat until no new reports are generated or you feel fine blocking the items you’re seeing.

Phase 3 – Enable Blocking

Now that you feel comfortable that you’re not going to break your site for any of your users, you can enable the policy using the “Content-Security-Policy” header and copy-and-paste the settings you’ve been testing.

<?php
header("Content-Security-Policy-Report-Only: script-src 'self' https://cdn.jsdelivr.net;report-uri /csp-violation-reports.php");
header("Content-Security-Policy: script-src 'self' https://cdn.jsdelivr.net;report-uri /csp-violation-reports.php");
?>

And this will prevent the inline code from running when we refresh our page.

Hi … do something helpful from important.js..

Allowing Inline Safely

Now the best option for your CSP configuration is to not allow inline code or styles, but you’ll occasionally need to do so.

For example, we have the following page that only allows external scripts, but we need to have some code that we add dynamically.

<?php
header("Content-Security-Policy: script-src 'self'");
?>
<html>
    <script>
        document.write("dynamically inject some safe code<br>");
    </script>
    ...
</html>

This, of course, isn’t going to run.

To get around this, we can build a nonce, specify it as part of our CSP header, and then include it as part of the script tag like so.

<?php
$nonce = bin2hex(random_bytes(16));
header("Content-Security-Policy: script-src 'self' 'nonce-$nonce'");
?>
<html>
    Hi
    <script nonce="<?php echo htmlspecialchars($nonce, ENT_QUOTES); ?>">
        document.write("dynamically inject some code<br>");
    </script>
    ...
</html>

I don’t recommend this, as it opens a new vector for attack because someone might be able to get PHP code injected into your page, which will bypass the checks you’ve added.

Other Options

This article just starts to scratch the surface of using CSP.

So far, we’ve only talked about how to control how scripts load, but CSP allows us to control:

  • CSS: style-src
  • Images: img-src
  • External Connections: connect-src
  • fonts: font-src
  • applets: object-src
  • Audio/Video: media-src
  • iframes: frame-src

and more using the features we’ve discussed. There’s also a “default-src” option, which controls almost every option, which is the best option if you can use it.

There’s also a ton of options for defining what’s allowed and what isn’t, so don’t feel limited to what we have listed here.

https://content-security-policy.com/#source_list

What You Need To Know

  1. CSP allows you to limit what content is loaded from your HTML
  2. Ton of options
  3. At least set it up for scripts

 

Leave a comment

Use the form below to leave a comment:

 

Our Partners

Collaborating with industry leaders to bring you the best PHP resources and expertise