TLS/SSL Security In PHP: Avoiding The Lowest Common Insecure Denominator Trap
A few weeks back I wrote a piece about updating PHARs in-situ, what we’ve taken to calling “self-updating”. In that article, I touched on making Transport Layer Security (TLS, formerly SSL) enforcement one of the central objectives of a self-updating process. In several other discussions, I started using the phrase “Lowest Common Insecure Denominator” as a label for when a process, which should be subject to TLS verification, has that verification omitted or disabled to serve a category of user with poorly configured PHP installations.
This is not a novel or even TLS-only concept. All that the phrase means is that, to maximise users and minimise friction, programmers will be forever motivated to do away with security features that a significant minority cannot support by default. In the case of PHP users on Windows, this may include not having openssl or curl installed. Without either of these options, TLS verification in PHP becomes impossible without looking outside PHP (e.g. locally available system commands).
The problem is that while programming to the Lowest Common Denominator is fine for many things, doing so to the point of maintaining active security vulnerabilities is not. Let’s take the simple example of Composer. It’s an incredible tool, used by most PHP programmers I know, but it can’t perform TLS verification worth a damn despite operating primarily over HTTPS URLs. On Reddit, there is a another tool just announced which relies on Composer to update application modules. That inherits the same vulnerability by depending on Composer in a live server setting. So too will other Composer dependent tools merely by inheriting from or reusing its download classes. In time, you finally have people seeking refuge in authority because Composer does this, and look, everyone and their pet hamster still uses it!
There’s A Topic In Here Somewhere
Much as I did around writing phar-updater and, more importantly, documenting the reasoning behind a tool that enforces TLS and supports openssl signing as a first citizen, I’d like to drill down into the specifics of how to approach this problem. It’s not an insurmountable one assuming you accept some basic ideas:
- You should never knowingly distribute insecure code.
- You should accept responsibility for reported vulnerabilities.
- You should make every effort to fix vulnerabilities within a reasonable time.
- You should responsibly disclose vulnerabilities and fixes to the public.
These four ideas are self-explanatory as the guiding principles that any good security policy is founded upon. When you violate them, you earn general mistrust and reputational damage when your users either figure out that violations occurred, or that those violations contributed towards the worst case scenario: getting hacked and all the ugly outcomes that follow. You only need to go on Reddit and other news sites to find that Magento’s reputation is currently being ripped to shreds over failing to uphold these principles recently.
So, given something like an application where the expectation is that everyone will install it, whether it be on Ubuntu, Windows, or Terminator-X45, how does one go about implementing TLS verification as securely as possible without being overly burdensome on programmers? Is it even possible?
Step 1: Implement TLS Verification
In keeping with those four ideas from earlier, the first course of action is to just implement TLS verification and get a handle on the consequences. Foisting a security vulnerability onto all members without their consent is irresponsible programming and should never be tolerated by the community.
It’s essential to reiterate that Insufficient Transport Layer Protection is a security vulnerability, making it possible for attackers to perform Man-In-The-Middle (MITM) attacks (this applies to intranets as much as on the internet). If it’s located and reported, it falls under your published security policy (if any) and the four central principles expressed earlier. There is a reason why URLs on the internet are prefixed with HTTPS.
We like to think of PHP as the programming language of the web, yet we continue to struggle and fight against a 20 year old protocol that underpins the security of users on that web.
Step 2: Identify The Consequences
All programmers spend a decent amount of time problem solving, and that should come into play now. For most people, there will be no unwanted consequences. They will have openssl/curl installed, and their operating system will have the necessary Certificate Authority (CA) certificates available. The only errors that they will experience will be the usual HTTP fare, and infrequent SSL errors for misconfigured remote servers (not the local system). Attempted MITM attacks will also, very obviously, generate errors assuming the TLS implementation and its dependencies are sound.
The less desirable consequences will then make themselves known. The most commonly quoted one is that Windows users will encounter errors because their local PHP does not have openssl or curl enabled. Other common issues are errors around locating the CA certificates necessary when verifying the remote server’s SSL certificate.
You now have two choices in implementing TLS. Enforce it or disable it.
First of all, you can’t just disable it because it would then put you in the position of deliberately introducing a security vulnerability. Leaving it enabled is not the end of the world. Just because some users have a poorly configured PHP, it does not immediately follow that all users should have their security compromised by default.
The more logical approach is to assess the local system prior to making remote requests. Those who pass muster will be fully secured, and those who don’t? We’ll get to them…
Step 3: Document Solutions & Stand Over Your Dependencies
TLS verification in PHP requires openssl or curl (or both depending on the application dependencies). Short of falling back to secure local system options on the command line, this is an unavoidable part of programming in PHP. So, when users don’t have the requisite extensions, and you have no other fallback to hand, you should simply start by telling them this.
When it comes to missing extensions, the solution is generally just a minor php.ini edit away. On Windows, it’s often just adding or uncommenting a line like “extension=php_openssl.dll”. Don’t only give users an “openssl not installed”, or worse, a puzzling one liner lifted from a PHP error message. Provide some information as to what is missing, why it is required, and a link as to where to find help.
This brings us to documentation. Most of the dependency issues have very simple solutions, editing a line or two in php.ini, or installing/downloading a CA certificate pack. Those extension DLL files are normally available regardless of how you get PHP. You can summarise the common solutions on your website or wiki and include the link in any error or feedback messages within the application output.
Step 4: Let Loose the Big Red Box of Doom?
To this point, you’ve avoided introducing a security vulnerability and have done your best to enable users to fix their dependency and configuration issues. They still want to use your application despite not following your recommendations. Perhaps it’s time to make it possible for users to shoot themselves in the foot without your assistance?
In a CLI application, for example, you can create a new “—disable-tls” flag which disables TLS protections when set. Whenever it is used, a very obvious, very unavoidable and very red box is displayed, informing the user that TLS protections have been disabled for the current command.
Text along the lines of “The end of the world is nigh!” would probably be too much. Mentioning that they are now vulnerable to Man-In-The-Middle attacks would simply be fact.
At its crux, this article is as much about user consent as anything else. Distributing code which simply turns off all TLS protection (or indeed, any security protection) without a user’s knowledge is irresponsible. Enforcing it but allowing for users to opt-out of that protection after seeing an informative warning is not quite as dire. Some security professionals will still be unhappy with the idea of ignorant users just wanting all the errors to go away and being able to achieve that, but this is actually an approach that exists in modern browsers whenever an invalid SSL certificate or TLS error is experienced.
At some point, users need to take on a bit of the responsibility for their own protection.
Doubtlessly, this would inconvenience some users. Extra settings or CLI flags might not be immediately obvious, digging through a php.ini to find those extension lines is an inconvenience, and red warning boxes everywhere can be annoying, but the alternatives as they popularly exist in PHP today need to go extinct. This idea of disabling security by default, of programming to the Lowest Common Insecure Denominator, without anyone’s consent or knowledge is neither responsible nor sustainable.
As Gandalf would say, “Keep it secret, keep it safe”. Just putting it on a table in plain view for the Sackville-Bagginses to steal is neither!