June 29, 20210 mins read
Following on from my previous post on testing for PHP Composer security vulnerabilities, I thought this post might be useful in helping create more secure applications that prevent PHP code injection. As developers, we build apps to help make end users' lives easier. Be it entertainment, workplace or social network application, the end goal is to protect the users we build for by ensuring we build security into the code. Adopting secure coding practices not only helps keep end users secure, it also saves time and development costs by preventing rework.
What is code injection?
How to prevent code injection in PHP
Code development security begins at the code front with the adoption of a secure coding convention. Be aware of front facing inputs and how they are handled. Sanitise everything coming in and be aware of how it’s stored within your application.
As a fundamental rule, dynamic code execution should never be allowed in any application. For example, avoid using core PHP functionality like
exec() where possible, which executes at the OS level.
It's highly recommended to always be using an open source security scanning tool like Snyk Open Source which will scan your package manager to identify potential vulnerabilities in library and dependency use. Something I've also been doing daily is checking the composer Snyk Vulnerability database to explore the techniques being used. The detailed report pages will allow you to trace back to repositories at the code level to explore how the vulnerabilities were identified.
5 ways to prevent code injection in PHP app development
Use a PHP security linter
Utilise a SAST tool to identify code injection issues
1. Avoid using exec(), shell_exec(), system(), or passthru()
As the saying goes “here be dragons.” As a rule, Avoid the use of anything that can directly call the operating environment from PHP when possible. From an attack vector point of view this has lots of opportunities to open all sorts of issues directly into your stack.
Traditionally, functions like
passthru() would be utilised to perform functions like compressing or decompressing files, creating cron jobs and even navigating OS files and folders. These all will run without any built in code sanitisation, which is where the trouble begins in using them particularly with unvalidated or sanitised direct user input.
PHP does provide some functional operators with built-in input escaping, namely
escapeshellarg(), which will pass input through some level of escape and sanitisation as part of the function call. There are a few more secure ways to approach the same functionality.
As of PHP 7.4, archiving can be handled by the
ZipArchive class, activated with
--with-zip flag as part of a PHP compile. Extra care needs to be taken with this however as it still could enable a traversal attack.
Approach file system interaction programmatically by minimising the ways that it can be interacted with directly via input. PHP has file functions that can be utilised without needing to call the OS directly. Getting a list of files in a specified directory, for example, can be done via the
scandir() function, which will return an array of files available in a directory then opened or referenced within code itself. Remember to take care not to access files created by exposed input and also with some of the functions around file based permissions like
chown(). Also, it's important to not trust the content header (which can be faked) and to validate the file type.
Create cron jobs dynamically using a Composer library like cron. This is actually run via a cron which then runs its own crons as part of the overall cron runtime. Care still needs to be taken with what gets run in it but approaches process creation more programmatically without exposing access to the core OS.
2. Avoid using
strip_tags() for sanitization
Extra care should always be taken with user input sanitization and handling. The goal here is to accept valid user input but then to store and handle it in the right way so that your application does not become vulnerable. Remember, inputs are an open attack vector for malicious actors to be able to interact with your application. So make sure to take the time to always handle the input data properly.
htmlentities() is another option that gets used for sanitizing input which also allows for definable UTF charsets, keep in mind that this still does not sanitize input completely.
It's also possible to strip data down using a regex function like
preg_replace('/[^a-zA-Z0-9]+/', '', $text). This will only return only text and numerics from the input, for UTF-8 character sets. Care should also be taken with functions like
mb_strtolower() when handling user input cleaning. We've seen instances in the past where vulnerabilities have surfaced in some of the multibyte string functions (
mb_) like CVE-2020-7065, which is an out of bounds write vulnerability caused by UTF-32LE encoding. This can lead to a stack overwrite buffer crash and allow for code execution to occur.
Opt instead for using something like
filter_var() which will validate and sanitize based on the defined filters options. For example
FILTER_FLAG_STRIP_HIGH passed into the
filter_var() function along with user input for example will remove all HTML tags and all characters with ASCII value
There are also a few Composer libraries which are commonly used for input sanitization. A library like HTML purifier offers HTML compliant sanitization with fairly good allow listing that lets you customize for the type of input data you need to let into your application.
unserialize() in PHP
This whole section could quite easily be a whole blog post on its own and it has been a hotly debated topic over the years, even within the developers working on PHP. The PHP manual actually highlights the dangers of using the
unserialize() function. Particularly that it should “not be passed untrusted user input” which can cause “code to be loaded and executed”.
Unserialize() is intended to be used to convert a class to a string that can be stored and passed to other functions or cached for use later. On its own, it sounds relatively harmless until you start to understand how the underlying C code works when storing
unserialize() data in memory, which is where most of its issues then can happen.
It is highly recommended to use a standard data format such as JSON via
json_encode(). This can provide a sanitized and safe transport method for sending and receiving serialized data to and from the user.
4. Use a PHP security linter
Having the right tools in place as part of your development workflow helps create secure and more functional code right from the start. Linters are a great way to reduce errors and potential issues as part of PHP application development and also reduce vulnerabilities in source code.
It's also strongly advised to make sure display errors are turned off by default in PHP.ini configurations. Disabling
error_reporting = E_ALL,
~E_WARNING will remove error output which could be potentially used to identify environment and configuration information within your application.
The PHP language itself has a linter built in which will display very verbose error messages as part of its validation. It can be called at the CLI (or as part of testing frameworks) by running
PHP -l followed by the file to check. The downside to using this as a linter is it can only check one file at a time, although it can be used as part of a loop to iterate over multiple files in a single run.
PHPlint is a robust, more widely known option for checking multiple files quickly. PHPLint can be used at the CLI or as a composer instigated library. Alternatively it can be called into a docker image fairly easily. PHPLint can be used to check PHP 7 and PHP 8, and while it has fairly verbose output, it can be used to identify issues via several linting processes at once.
Another popular linter is PHP-Parrallel-Lint, which supports PHP 5.3 to PHP 8.0 and is also able to run multiple linter processes quicker than you can type
echo "hello world". PHP-Parrallel-Lint has more detailed output than the aforementioned options, although it does not yet support a CLI option or an out-of-the-box Docker solution at the moment.
5. Utilize a static application security testing tool to identify code injection issues
One of the more widely used IDEs for PHP development is PHPStorm which has some really handy static code analysis features built right in. Building on the linters we explored above, PHPStorm has integration options for Xdebug, which is great for linting, profiling and (as the name suggests) debugging code to reduce errors. For secure coding workflows PHPStorm allows the enforcing of specific coding rules and PSR standards, Which is handy especially working on PCI compliant applications.
To improve code security, Snyk has a plugin for PHPStorm which can help scan for code quality issues and vulnerabilities in the code and libraries in your code. It's easy to install and get up and running really quickly and is available inside the plugins section within the app.
Another development tool which is gaining wider adoption amongst developers, and particularly PHP developers, is VSCode. Like PHPStorm, it also has a built-in linter and the ability to enforce PSR standards.
Scanning for vulnerabilities in VSCode with Snyk is as simple as opening a new VSCode terminal and using the Snyk CLI to snyk test or use snyk monitor to get ongoing alerts to the code base. To find out more to get started with Snyk CLI and PHP, check out my previous blog post on testing for PHP Composer security vulnerabilities with Snyk.
Update: We have also recently released PHP support for Snyk code, to read more and try it for yourself see the blog post Snyk Code support for PHP vulnerability scanning enters beta
As developers, we build applications to help end users in their day to day life. Making sure those applications are kept secure not only ensures users that their data is safe, but also that they can trust you.
Ensuring you identify code injection vulnerabilities while building applications at the code front is easy, providing you have the right tools and workflow at the start.