Jump to content

(Pretty) easy A+ Content Security Policy (CSP) for Processwire


Chris Bennett
 Share

Recommended Posts

Plenty of posts on the forum relating to Content Security Policy (CSP) and how to integrate it with Processwire.

It's not too hard to implement a decent htaccess CSP that will get you a solid B+ at Mozilla Observatory.
If you're after A+ it's a little harder because of all the back-end stuff... until you realize it's surprisingly easy.

After a lot of testing, the easiest way I found was to specify only what is needed in the htaccess and then add your required CSP as a meta in your page template.
Plenty of people have suggested similar. Works very easily for back-end vs front-end, but gets complicated if you want front page editing.

Luckily, a little php will preserve back-end and front page editing capabilities while allowing you to lock down the site for anyone not logged in. 
None of this is rocket science, but CSPs are a bit of a pain the rear, so the easier the better, I reckon ?

The only CSP I'd suggest you include in your site htaccess is:

Header set Content-Security-Policy "frame-ancestors 'self'"

The reason for this is you can't set "frame-ancestors" via meta tags.
In addition, you can only make your CSP more restrictive using meta tags, not less, so leaving the back-end free is a solid plan to avoid frustration.

Then in your public front-facing page template/s, add your desired Content Security Policy as a meta tag.
Please note: your CSP should be the first meta tag after your <head>.

For example:
 

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="Your CSP goes here">
<!-- followed by whatever your normal meta tags are -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no">

If you haven't got Front Page Editing enabled, this works fine by itself.
Just one extra step is needed to make sure you don't have to worry either way. 

The easiest way I found to allow both CSP and front page editing capabilities is the addition of a little php, according to whatever your needs are.
Basically, if the user is a guest, throw in your CSP, if they're not do nothing.
It's so simple I could have kicked myself when it finally dawned on me.
I wish it had clicked for me earlier in my testing, but it didn't so I'm here to try to save some other person a little time.

Example:

<!DOCTYPE html>
<html>
<head>
<?php if ($user->isGuest()): ?>
<meta http-equiv="Content-Security-Policy" content="Your CSP goes here">
<?php endif; ?>
<!-- followed by whatever your normal meta tags are -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no">

 

If you want it a bit more involved then you can add additional tests and be as specific as you like about what pages should get which CSP.

For example, the following is what I use to expand the scope of the CSP only for my "map" page:

<?php
$loadMap = $page->name === "map";
?>
<!DOCTYPE html>
<html>
<head>
<?php if ($user->isGuest()): ?>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; base-uri 'self'; manifest-src 'self'; form-action 'self'; font-src 'self' data: https://fonts.gstatic.com; frame-src 'self' https://www.youtube.com; img-src 'self' data:<?php echo ($loadMap) ? " https://maps.googleapis.com https://maps.gstatic.com" : ""; ?> https://www.google-analytics.com; script-src 'self' <?php echo ($loadMap) ? "https://maps.googleapis.com " : ""; ?>https://www.google-analytics.com https://www.googletagmanager.com; style-src 'self' <?php echo ($loadMap) ? "'unsafe-inline' https://fonts.googleapis.com" : ""; ?>">
<?php endif; ?>

 Hope this saves someone a little time testing.

https://observatory.mozilla.org/analyze/bene.net.au

  • Like 16
Link to comment
Share on other sites

You are right; whilst it is possible to get a vhost/.htaccess-based A+ on security headers while keeping the admin interface fully operational, it is a pain and leaves the apache config files in a bit of a mess. Getting the admin interface sorted in the vhosts file prompted this post over in dev-talk that might also help someone out if they go down that route.

What you've posted seems to be a fairly nice approach to the issue - thanks!

  • Like 1
Link to comment
Share on other sites

  • 1 year later...

2021 update: bit easier, better security, no warnings about using <meta http-equiv="Content-Security-Policy">.

Rather than rely on <meta http-equiv="Content-Security-Policy"> I have tweaked things a little.
Bit more secure, bit easier to do and added nonce function as well to further lock down script-src.

If we remove all CSP from htaccess we can define everything in one place and set header() with php instead, right before <!DOCTYPE html>
I find it cleaner and easier to customize that way. Allows as much logic and conditional loading as you need.

For example:

<?php if ($user->isGuest()):

$loadMap      = $page->name === "map";
$hasMapImgs   = ($loadMap) ? " https://*.googleapis.com https://maps.gstatic.com https://*.ggpht.com" : "";
$hasMapStyles = ($loadMap) ? " 'unsafe-inline' https://fonts.googleapis.com" : "";
$cspImgSrc    = " img-src 'self' https://www.google-analytics.com data: 'self'" . $hasMapImgs . ";"; 
$cspStyleSrc  = " style-src 'self'" . $hasMapStyles . ";";

$cspCond  = $cspImgSrc.$cspStyleSrc;

function generateRandomString($length = 25) {
    $characters = '0123456789';
    $charactersLength = strlen($characters);
    $randomString = '';
    for ($i = 0; $i < $length; $i++) {
        $randomString .= $characters[rand(0, $charactersLength - 1)];
    }
    return $randomString;
}
$nonce = generateRandomString(35);

$csp = "Content-Security-Policy: base-uri 'self'; frame-ancestors 'self'; default-src 'none'; child-src 'self'; manifest-src 'self'; form-action 'self'; connect-src 'self' https://www.google-analytics.com https://maps.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; frame-src 'self' https://www.youtube.com; script-src 'self' https: 'unsafe-inline' 'nonce-" . $nonce . "' 'strict-dynamic';" . $cspCond ;

header($csp);
?>
<?php endif; ?> 
<!DOCTYPE html>

https://observatory.mozilla.org/analyze/www.alfresco-bar-bistro.com.au

  • Like 2
Link to comment
Share on other sites

19 minutes ago, DV-JF said:

@Chris Bennett is your approach compatible with ProCache or are there any edge cases that have to be considered?

Sorry @DV-JF I can't be sure as I don't use ProCache.
I can't imagine why it would, but I just can't say for sure. If there was a problem, I guess nonces could be replaced by URLs instead.

Naturally, you'd want to make sure your CSP settings are suitable for your site or the devtools will quickly fill with lots of red :)

  • Like 1
Link to comment
Share on other sites

@Chris Bennett Thx for responding.

What makes me think a bit is the following circumstance: if I understood the specification (https://content-security-policy.com/nonce/ - see "Rules for Using a CSP Nonce") correctly, it is necessary that for each "HTTP response" nonce must have a unique value. However, this will not work if ProCache caches the pages.

Maybe someone else has an idea about this?

Link to comment
Share on other sites

5 hours ago, DV-JF said:

@Chris Bennett Thx for responding.

What makes me think a bit is the following circumstance: if I understood the specification (https://content-security-policy.com/nonce/ - see "Rules for Using a CSP Nonce") correctly, it is necessary that for each "HTTP response" nonce must have a unique value. However, this will not work if ProCache caches the pages.

Maybe someone else has an idea about this?

Yes, you are absolutely correct that the nonce should be unique for each HTTP request.  The nonce is basically just a handy way to load a number of scripts from different domains and have those permissions filter through to other scripts that might be loaded by the allowed scripts.

So for a static page you'd probably want to go with the url allowlist method instead, similar to the $hasMapStyles / $cspStyleSrc combo.

Link to comment
Share on other sites

  • 1 year later...
On 11/19/2021 at 9:54 AM, DV-JF said:

Maybe someone else has an idea about this?

It's not exactly the same technique, but I have just got into this rabbit hole and after a bit of blood sweat and tears and help from a pal, this is working for me while using ProCache and using the nonce attribute on scripts.

This configuration assumes you have mod_substitute and mod_cspnonce installed.

<If "%{THE_REQUEST} !~ m# /processwire/?#">
  Options +Includes
  AddOutputFilterByType SUBSTITUTE;INCLUDES text/html
  Substitute "s|--CSP-NONCE--|<!--#echo var=\"CSP_NONCE\" -->|i"
  # Customize to your needs
  Header add Content-Security-Policy "default-src 'self' 'nonce-%{CSP_NONCE}e';
</If>

Place this at the end of the ProcessWire htaccess directives. 

This should swap on the fly the Apache response and replace any --CSP-NONCE-- script.

Then on Apache you can also set the nonce headers like this: 

 

I do not use this because my server setup uses nginx as reverse proxy.

For example:

<script nonce="--CSP-NONCE--" src="https://totally-safe-website.com"></script>

Will end up as:

<!-- nonce swapped on every request! -->
<script nonce="0O4I3O5nNFG/MVpqormzyIuH" src="https://totally-safe-website.com"></script>

@ryan fyi 

In theory, this would be A LOT simpler in Apache 2.5, since you could put an expression within the Substitute directive instead of the server side includes to substitute the "--CSP-NONCE--" script, but right now I'm limited to Apache 2.4 in the setup where I need this since I don't have control of the stack versions. So this should work in Apache 2.4+

  • Like 3
  • Thanks 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...