Secure Headers for PHP

1 Jan, 2017

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.

Why?

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?

Maintenance

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.

Content-Security-Policy

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:

$headers->csp('style', 'https://awesomestylesheetcdn.example.com');

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:

$headers->strict_mode();

'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?

Cookie upgrades

Session cookies often lack the HttpOnly or Secure flags set to protect them from javascript (specifically javascript inserted via XSS) and being read while in transit on any insecure requests made by the browser.

SecureHeaders will look for any cookies matching the following names/substrings. (Obviously both these lists will grow over time).

Substrings

sess
auth
login
csrf
xsrf
token
antiforgery

Names

sid
s
persistent

If any are found, then SecureHeaders will do what amounts to injecting the HttpOnly and 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.

Safe Mode

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 includeSubDomains or preload until the header is manually added as a safe mode exception, or safe mode is disabled.

Automatic Defaults

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

Here's some exhaustive documentation.

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.

Why PHP?

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.

PHP usage statistics chart

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.

Feedback

If you have any feedback (ideas or feature requests/comments/bugs/etc...), the project has an issue tracker (feature requests welcome there too), or give me a shout on Twitter @aidantwoods.

1 Jan, 2017

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.

Why?

If you're familiar with PHP, you'll know that...[read more]

27 Aug, 2016

Disclosure and Google's Response

This one feels very strange writing, because the vulnerability detailed below is currently exploitable. Google has been notified of this vulnerability, yet they have chosen to do nothing.
GoogleThanks for your bug report and research to keep our users secure! We've investigated your submission and made the decision not to track it as a security bug.
In hope that public disclosure will encourage Google to do otherwise, here goes...[read more]
22 May, 2016
Biometric sensors are good at identity. They can tell you exactly who a fingerprint belongs to; who's face is in-front of the camera; even who's DNA was left at a crime-scene. But biometric sensors are not good authenticators.

Replay Attacks

Let's say Bob, thinks it's a good idea to add a voice activated lock to his house...[read more]
4 May, 2016

Preamble

A little over a month ago I contacted my bank (Santander), asking them why they served me their homepage insecurely...[read more]
2 May, 2016
In this post I'll address one specific task: obtaining a manual certificate from Let's Encrypt.

Who is this for?

If you don't have direct command line access, or the necessary permissions to install Let's Encrypt on your webserver; then you won't be able to obtain a certificate using the automated process.
If you can install Let's Encrypt on your webserver, you should. It simplifies the process down to a single command. You'll also enjoy the benefits of being able to setup an auto renew process directly on the machine serving the certificate...[read more]