Recently I've been working on a drop in class to manage certain "Secure Headers" in PHP.
By "Secure Headers", I'm of course talking about those mentioned in the OWASP Secure Headers Project.
The project, SecureHeaders is available on GitHub.
If you're familiar with PHP, you'll know that adding a header is actually quite easy. For example, HSTS can be configured in a single line as follows:
header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
In-fact, you could configure any one of these headers in exactly the same way. So why use a 2.3k line PHP class instead of a one liner fix for each header?
I started the project as a small class to use for myself to manage CSP policies.
At the time of writing, this is my CSP string:
default-src 'none'; script-src 'self' https://www.google-analytics.com/ 'nonce-noWJFLxtYDQCaRhA3wzbpnnj0ayxstr6mVat+VcB' https://platform.twitter.com/js/ https://cdn.syndication.twimg.com/tweets.json 'strict-dynamic'; style-src 'self' https://fonts.googleapis.com/ https://cdnjs.cloudflare.com/ajax/libs/font-awesome/ 'nonce-AzuI9nGHk86GV7NJ5LNZdsKE7mJeUlDPggnW1/R8' https://platform.twitter.com/css/ https://a.disquscdn.com/next/embed/styles/; img-src 'self' https://www.google-analytics.com/ https://platform.twitter.com/css/ https://syndication.twitter.com/i/jot/syndication https://syndication.twitter.com/i/jot https://pbs.twimg.com/ https://i.ytimg.com/vi/ data: https://referrer.disqus.com/juggler/stat.gif https://a.disquscdn.com/next/embed/assets/img/; font-src 'self' https://fonts.googleapis.com/ https://fonts.gstatic.com/ https://cdnjs.cloudflare.com/ajax/libs/font-awesome/; base-uri 'self'; connect-src 'self' https://www.google-analytics.com/r/collect; frame-ancestors 'none'; object-src 'none'; block-all-mixed-content; upgrade-insecure-requests; report-uri https://aidanwoods.report-uri.io/r/default/csp/enforce; child-src https://syndication.twitter.com/ https://platform.twitter.com/ https://www.youtube.com/embed/ https://disqus.com/embed/comments/ https://disqus.com/home/preload/ https://disqus.com/home/forums/aidanwoods/ https://disqus.com/home/inbox/; frame-src https://syndication.twitter.com/ https://platform.twitter.com/ https://www.youtube.com/embed/ https://disqus.com/embed/comments/ https://disqus.com/home/preload/ https://disqus.com/home/forums/aidanwoods/ https://disqus.com/home/inbox/; form-action https://syndication.twitter.com/ https://platform.twitter.com/;
Obviously, this is not a format that lends itself nicely to debugging and maintaining. While some URIs might be obvious as to their purpose upon reading, things like
https://pbs.twimg.com/, for example, are not. Trying to review that entire string isn't really a pleasant experience. What I wanted to do is break down the different parts of the policy into distinct sections.
If you visit my AMP supported webpage, then the CSP string is even longer.
Rather than including that string too, the difference between these two strings is much better expressed as an associative structure. This ended up being my initial method for doing that:
Another reason to use an associative structure: multiple polices can't be combined by simply appending strings. Sources need to be filed under the correct directives.
In the current version of SecureHeaders, the code is now something like this:
$headers->csp($amp_csp); $headers->csp($blog_post_csp); $headers->csp_hash('style', $amp_css);
Personally, I think that's miles better than the equivalent using PHPs header function:
header( 'Content-Security-Policy: ' . 'default-src ' . $amp_csp_default_src . ' ' . $blog_post_csp_default_src . '; ' . 'script-src ' . $amp_csp_script_src . ' ' . $blog_post_csp_script_src . '; ' . 'style-src ' . $amp_csp_style_src . ' ' . $blog_post_csp_style_src . ' ' . "'sha256-" . base64_encode(hash('sha256', $amp_css, true)) . "'" );
For a start, it's more readable in my opinion. The above PHP
header function method is a bit of a simplification too, in reality you'd have to keep adding more blocks for each directive you wanted to merge.
But readability is just of part of it really. Something you can't do using PHPs built in
header function, is work in an append mode. With SecureHeaders, I simply run the following to add an additional CSP source:
If you wanted to append using the built in
header function you'd have to do it manually by completely rewriting the header.
Something else you won't find in PHPs built in header function is a sanity check on the policy you just added.
For example, if I do something like:
$headers->csp('script', 'unsafe-inline'); $headers->csp('script', 'http://insecure.cdn.org');
I'll start seeing some warnings appearing at the top of my page (generated at level
E_USER_WARNING, which (along with other error messages) should probably be turned off on a deployed application):
Warning: Content Security Policy contains the 'unsafe-inline' keyword in script-src, which prevents CSP protecting against the injection of arbitrary code into the page.
Warning: Content Security Policy contains the insecure protocol HTTP in a source value ** http://insecure.cdn.org **; this can allow anyone to insert elements covered by the script-src directive into the page.
I can enable opportunistic use of
strict-dynamic in CSP using:
'Opportunistic' here meaning that
strict-dynamic will be utilised only if a nonce or hash is included in the applicable directive.
(This mode also sends the HSTS header with preload criteria, see documentation for how to change this, and the exact criteria used to determine which directive
strict-dynamic is inserted into).
I've also included nonce and hash generators in SecureHeaders. I feel the nonce generator is an especially important addition, it avoids the pitfalls associated with non-secure sources of randomness that may be used in PHP, by utilising the commonly available OpenSSL random-pseudo-bytes. SecureHeaders will also generate a warning if OpenSSL reports that it could not use a cryptographically strong algorithm to generate the nonce.
Okay, so that's an overview of some of the things you can do in CSP.
There is support for HSTS and HPKP too, as well as the report only modes of HPKP and CSP.
What else does it do?
Session cookies often lack the
SecureHeaders will look for any cookies matching the following names/substrings. (Obviously both these lists will grow over time).
sess auth login csrf xsrf token antiforgery
sid s persistent
If any are found, then SecureHeaders will do what amounts to injecting the
Secure flags for each cookie.
In reality, all the
Set-Cookie headers must be removed (there's no way to remove just a single header as they all have the same name). Before they're removed, their values are parsed to determine cookie properties. These properties are then amended as appropriate, then all the
Set-Cookie headers are set again.
The substrings and names can of course be modified to add or remove entries as needed.
To attempt to combat accidental use of security features that have long term effects, I've included safe mode (not enabled by default) which will place an upper limit on things like HSTS and HPKP, and remove flags like
preload until the header is manually added as a safe mode exception, or safe mode is disabled.
Cookie upgrades will default to on for the list of substrings mentioned in the two-previous section. Some headers will also be automatically added if they're not explicitly set or removed.
X-Powered-By will be removed automatically (lets avoid disclosing internal version numbers to a potential attacker). Certain policies will also generate warnings or notices based on a sanity check of their values. Though this is of course not exhaustive – SecureHeaders does not look for JSONP endpoints like Google's much more exhaustive evaluator, for example.
A lot more
How does it work?
The full source code is available on GitHub for the very specific details.
Here's an overview:
- Headers are configured using SecureHeaders' policy managers, or using functions to add/remove headers
- SecureHeaders receives notice that header configuration is now complete (this can be setup to trigger when output starts getting sent to the browser)
- SecureHeaders imports all the headers from PHPs internal list, PHPs internal list is cleared (this forces all headers under PHP jurisdiction to undergo checks by safe mode, and allows cookies to be modified by SecureHeaders, as well as polices to be imported)
- Automatic behaviour is applied (this includes adding/removing/modifying headers and cookies either due to defaults, or things that are configured – like strict mode)
- Policies are compiled and added to the header list (headers are not added past this point)
- Headers staged for removal are removed
- Safe mode is applied to all remaining headers (if enabled)
- Headers are put back into PHPs internal list to be sent to the browser (headers are not modified past this point)
- Warnings/notices are generated based on a lack of certain security headers, or via feedback generated by header sanity checks
SecureHeaders is also 'compatible' with PHPs normal
header function, meaning that anything added using PHPs function will be imported into SecureHeaders so that polices can still be appended together, and sanity/ safe mode checks can still be performed.
Code written in PHP is notorious for having security issues (usually unrelated to PHP specific vulnerabilities, and more to do with common pitfalls). PHP is also by some estimates the most popular language on the web.
While SecureHeaders obviously won't do anything to remove issues like SQLi etc... it will hopefully make use of built in browser security features much easier, and more manageable – which will help to combat things like session hijacking, and XSS.
Current State of the Project
The project has got to a stage where I'm almost happy enough to draw a line on syntax by writing an interface to stick to. I'll leave it in a "beta" stage for the moment though, with the possibility of breaking backwards compatibility in the near future. All backwards incompatible changes from here on out will be pointed out in the release notes. I'll be following a Semantic Versioning naming scheme, so you can spot them in the version number too.