Keeping Credentials Secure in PHP

enygma

Chris Cornutt

Posted on July 4, 2019

Keeping Credentials Secure in PHP

** Previously posted on my site, Websec.io

One of the most difficult things in any kind of application (not just web applications) is how to protect "secret" values. These values might be API keys, database passwords or even special bypass codes. Ideally, you're not having to define these directly in the application and can have them loaded from another source.

While a lot of the issues around protecting secrets can be removed by better secret handling, it seems like there's still always a need for some kind of secret value to exist in an application. Using this sort of pattern is, obviously, recommended against. The Common Weakness Enumeration database even has an entry specifically about it: CWE-798. Hard-coding credentials, especially plain-text ones, can be a huge risk if an attacker were able to somehow access the code and read them directly.

So what about PHP?

In PHP applications there's a common pattern to keep configuration values and access details in a .env file that resides in a place where the PHP application can reach it. Given that this is a common practice, I didn't want to stray too far away from it. I want to provide something useful here that can easily replace this setup and still keep things as simple as possible.

I'm going to go through several methods of credential storage, spend some time talking about what it is and the good and bad about it. They're all going to make use of simple storage methods, either based in the code or in a related flat file (like a .env). We'll start off with the worst possible method - storing the plain-text credentials in the code.

Putting credentials in the code

It's very easy as a developer to think that since you need the credentials to, say, make a connection via a HTTP client to an API that keeping the credentials as close to this code as possible is the easiest and best solution. There are two big things wrong with this approach:

  1. These hard-coded credentials will now exist in your version control. If it's on a public GitHub repository, you basically just exposed those credentials to anyone that can clone that repository.
  2. If an attacker was ever able to access the source code for your application, they would have direct access to the secrets without having to do any kind of decryption or reversing work.

IMPORTANT: If you have creds hard-coded in your files, refactor them out immediately. This is a bad security practice (and is mentions in both CWE-256) and the OWASP Top 10 (as A2) in the Broken Authentication item.

There have been several instances over the past several years, some with web applications and some with other kinds of apps/hardware, where default or hard-coded passwords were their downfall. AVOID this at all costs! There is a very, very very slim use case for having any kind of sensitive information directly in your code. Even then, there are usually other protection methods built in to ensure that those can only be used in a small number of circumstances.

.env file living in the document root

So, now that we've determined that we need to avoid hard-coded, plain text credentials in your code we need to figure out another way to store them. This is where the popular .env file comes in. Ever since the more modern age of PHP development has come around (thanks to tools like Composer) many frameworks and libraries have adopted the pattern of using a .env file to store application-specific settings. Naturally, this meant that eventually secrets and sensitive information found it's way in there.

Having all of this in a separate file is better than having it hard-coded but there are still some issues. If you re-read the title of this section, you might find the issue. Remember, anything that's inside of your document root is directly readable by the outside world. In this case, the choice was made to put the .env file inside of the document root. That means that I could hit http://mycoolsite.com/.env and be able to access this file directly.

So, we can strike this one off the list as far as a method for storing anything we need to be protected. There's another similar option, however, that can prevent direct access: moving the .env file out of the document root.

.env file living outside of the document root

In this case, we're moving the file up one level so it can't be directly accessed from the web. For example, if your document root is /var/www/mycoolsite/public then you can move the file up one level at /var/www/mycoolsite/.env. This makes it so that PHP can still access the file but it can't be reached via the web.

For example, we could use the popular vlucas/phpdotenv package to read the file and automatically import it into the current environment:

<?php
require_once __DIR__.'/../vendor/autoload.php';

$dotenv = new Dotenv\Dotenv(__DIR__.'/../');
$dotenv->load();
?>

In this script, the code is told to load the .env file from one directory up (__DIR__.'/../'). It then pulls in the key/value combinations and puts them into the $_ENV superglobal. This method is better than having the file publicly accessible but there are also some downsides to consider:

  1. If an attacker is able to upload a PHP file and execute code, they could just print out the $_ENV values and have direct access.
  2. The values still exist on disk in plain-text so if a local file include issue is found the file can still be read.

So this method is a step in the right direction, but we still could use something a bit more robust to protect our credentials.

Encrypted credentials

The next step in preventing direct access to the secret values is to use either a method of obfuscation or encryption to protect the value. Since we'll need the plain-text version of the value to actually use it, obfuscation is out of the question. Using encryption we can encrypt the value and then decrypt it when needed.

We're going to build on the previous example and put the values in a .env file located outside of the document root. In order to help make the encryption/decryption process simpler, we're going to make use of the defuse/php-encryption library.

First, we need to install it and generate a key:

composer require defuse/php-encryption
vendor/bin/generate-defuse-key

This will give a key that contains upper and lowercase letters and numbers and has sufficient entropy to be used for this simple operation. This key will need to be stored where PHP can access it but not someplace in (or even close to) the document root of the application. A common practice is to put it somewhere under /usr/local in a flat file. This file then needs the permissions and owner/group changed so that PHP can read it.

Once you've set up that file, you can then read and decrypt the values from the .env file. We'll use the same vlucas/phpdotenv library to read in the file and then php-encryption to decrypt it. First the example .env file:

test=def502003cbef858698bc40b2b8d0ffb6f365f2cef00009047650910941da72372313c7ce3f9d4ce8ba2cd64f6a5a5a330da47151c5c90124fd4e8ea792d40810d8906b8a888b12db78f1cbb0819825447ce685b1c608dfb1f30
test1=def502005c647492189c68d7f5fec781a0e10bdee8865b23f729b080c7bbadd2204005367ea6464d75609ea48be235886cd2f398bf60eaa0a0bb32e2906ab9b9b1f66c58fdd24f054b5311460fdf8770c5d729b3c296cb5d

Then the PHP to decrypt it:

<?php
require_once __DIR__.'/../vendor/autoload.php';

$dotenv = new Dotenv\Dotenv(__DIR__.'/../');
$dotenv->load();

$keyContents = file_get_contents('/usr/local/keyfile`);
$key = \Defuse\Crypto\Key::loadFromAsciiSafeString($keyContents);

$secret = \Defuse\Crypto\Crypto::decrypt($ciphertext, $key);
?>

Our decrypted secret value then ends up in the $secret variable. Obviously, you wouldn't want to have to copy and paste this code all around so it'd be simpler to wrap it in a helper function or class to make it more self-contained.

This is yet another step in the right direction in protecting our credentials but there's still an issue here that's common to all of these flat-file storage methods: the local file include. If an attacker is able to make your code read and expose file contents, that means it could not only read the encrypted values from the .env but also the contents of the keyfile since PHP needs to be able to read that too.

We're running out of options here but let me suggest one more. This method still allows you to store the values in a flat-file but protects them from local file include attacks as PHP doesn't need to be able to access the file they're contained in, only the Apache web server. Let's get started.

The "Apache Pull" Method

In this method, we're going to use some of the same kind of techniques as before (storing the secrets encrypted in a flat-file) but there's a new twist: making use of Apache environment variables to relay those values to PHP.

NOTE: This tutorial shows how to set up an Apache web server but this same approach can probably be performed via Nginx as well.

If you want the quick and easy version, I've already set up this repository with a Docker-based example showing how the environment needs to be configured.

Here are the steps the code and applications will follow:

  1. A file is created containing the encrypted credentials somewhere on the file system (in this case we're just putting it in /tmp)
  2. This file is then sourced in the /etc/apache2/envvars file as an additional source pulling in these values as local environment variables.
  3. When Apache starts up it pulls in all of the values from envvars and redefines them internally. This includes our special values.
  4. These values are pushed out to PHP via Apache environment variables through a SetEnv statement.

The question you may be asking now has to do with those pesky local file include issues. Can't the additional settings still be read by PHP? That's where the last piece of the puzzle comes in: the open_basedir configuration. With PHP, you can use open_basedir to set the directories that PHP can interact with and keep it from going outside of those. In this case, we can lock PHP down to just the document root and prevent it from reaching out and getting the file manually.

Before I go on and show how it works, I do want to say one thing - this solution isn't perfect either. If an attacker were able to execute PHP code and read from the $_ENV superglobal, the key value would still be exposed.

The Secrets

First, we'll set up the secrets and get them sourced correctly. In our /tmp/addl-settings file, we've defined the key value:

export ENC_KEY=1234567890 // This is just a sample key, obviously

Now we source this file in the envvars file at the bottom of the file:

. /tmp/addl-settings

When this is set up, Apache will then load the ENC_KEY value into its internal environment and make it available.

The Apache Config

Next up is the Apache configuration. In this case, we're going to make use of virtual hosts but you could also do this at the base level:

<VirtualHost *:80>
    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined

    SetEnv ENC_KEY ${ENC_KEY}
</VirtualHost>

In this configuration you can see the special ${} notion that's used to pull an Apache environment variable in and make it available to the running process. In PHP this means making it a value in $_ENV.

Open_basedir

The final step of the puzzle is set up the open_basedir protection so we create an open_basedir.ini file and copy it to the right place for Apache to read it as a PHP ini configuration file:

open_basedir=/var/www/html

With all of this in place, the ENC_KEY value - our encryption key - is now available to PHP via Apache but cannot be accessed directly as a file.

But wait, there's more!

This setup is great and all but you might be asking yourself "How to I read my encrypted configuration settings now"? Well, with the help of a handy library - psecio/secure_dotenv that takes care of a lot of the processing, it's much simpler. We already have the key we need for encryption and decryption in the environment so we'll just reuse that for these examples. First, install the package using Composer:

composer require psecio/secure_dotenv

Then, in your application, create the new Parser instance feeding in the environment variable with the path to the key file:

<?php
$envFile = __DIR__.'/.env';
$parser = new \Psecio\SecureDotenv\Parser($_ENV['ENC_KEY'], $envFile);
?>

Reusing the .env file from the above examples gives us values for test and test1 which can be extracted from the result of a getContent call.

<?php
echo 'test1 is: '.$parser->getContent()['test1'];
?>

The library handles the decryption of the value for you behind the scenes (using the same defuse/php-encryption library) and you're left with a plain-text result.

Summary

Securing secrets in PHP applications is an interesting problem to tackle. In the research I did prior to this article, I found that - much like any other security-related topic - there's always more than one way to accomplish a task. PHP makes it even more difficult because of how it interacts with web servers. The PHP scripts and processing need to have at the least read access to every file they need to work with. This makes it very difficult to prevent local file include issues if you're not very careful.

There aren't any 100% secure options for credential storage but this "Apache pull" method I've shared here is one of the simpler methods that doesn't require much more than the technology you're already using. Of course, if you have a more complex environment that's deployed using Chef, Vagrant or other tools, those come with some additional features (like encrypted Chef databags) that can be used for credential handling as well.

Remember, there's no "one size fits all" solution for this. It depends a lot on your environment and the risk requirements for your application. Be sure to sit down and create an accurate threat model of your application before making a decision on how you're going to protect your secrets. This will give you a better overall picture of your needs and what kind (or kinds) of protection you'll need.

Resources

💖 💪 🙅 🚩
enygma
Chris Cornutt

Posted on July 4, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related