Secure PHP coding

Programming a web application securely is hard, partly because of the complexity and extreme interactivity, and partly because of the many insecure programming primitives available to developers. In the following, we discuss some general principles and specific solutions for secure PHP coding.

General principles

This is a non-exhaustive list of important principles for web secure coding:

  • Always pay attention to how user input is processed and prevent that user input modifies application control-flow in unexpected ways;
  • Avoid clearly insecure functions / coding;
  • Adopt security best practices whenever possible;
  • Avoid ad-hoc solutions when standard ones are available;
  • When no security solution is available, filter and sanitize input accurately, but remember that filter evasion might be possible.

Best practices for secure PHP coding

We review some important security best practices for PHP.

Use strict comparison

As we have discussed in a previous class, loose comparison == can introduce security flaws. Thus, it is important to use strict comparison === as much as possible. For example, in the following code, the attacker can bypass the token check by passing an integer 0 value as token, since == will consider it equal to any token beginning with a 0 or with a letter:

This can be fixed by simply replacing == with ===:

Now the comparison will be successful only if values and types are the same. Thus, sending an integer instead of a string will always make the comparison false.

It is also important to cast to the appropriate type when invoking a function. This is crucial to prevent unexpected behaviours like in the strcmp array vulnerability:

The problem here is due to the fact that strcmp returns NULL if one of the argumenta is not a string. Thus casting the user input to string will prevent the vulnerability:

Now, whatever is passed as input will be converted to string before it is compared with $token. In particular an empty array is (a bit surprisingly) converted into the string “Array” which will never match a random token in practice:

Even if now we are guaranteed that strcmp will always return an integer (i.e., loose comparison should be equivalent to strict comparison), it is always a good practice to get rid of loose comparison. In fact, by applying both principles above we get the following:

White-list included filenames

Including a file based on user input may allows severe attacks. Consider the simple example (from the password-protected demo):

The page uses the GET parameter p to include the specified page, for example selected from a menu. When the user clicks on “About” the particular page is loaded through:

Coding in this way is very dangerous. For example, the following request will display the content of /etc/passwd:

The next example uses the php://filter wrapper described here. In particular, php://filter can be used to apply filters to I/O as in the following example:

The effect is to read the resource contact.html and to apply string.rot13 filter, which implements a trivial rot13 encryption to the file. If we pass the above string to the vulnerable application the effect will be to display the encrypted page:

The interesting effect of using filters is that they allow for leaking the page source code, that would otherwise be executed if included:

Here, the PHP source code is based64-encoded before it is included, preventing its execution and making it possible to access it as source code.

All of the above examples work with a default PHP configuration. If allow_url_include is enabled much more attacks are enabled. For example, Remote Code Execution (RCE) is easily achieved through the data wrapper, which includes data directly in the page. Since data can be PHP code we get the following trivial attack:

If file inclusion cannot be avoided a good way to prevent attacks is to strictly white-list the filenames that are included. For example:

Notice that in_array by default uses loose comparison, again a very bad idea for security. To force strict comparison it is necessary to pass true as the third parameter.

When strict whitelisting is not possible it is necessary to resort to filtering in order to blacklist dangerous filenames. However this approach is risky because it is very hard to foresee any possible attack payload (see below some example of filter evasion).

Check integrity of unserialized data

PHP objects can be seralized and unserlized in order to store and resume them. Unserialization is a typical source of attacks on object-oriented languages. In fact, unserialization often triggers code execution. In PHP this happens through the so-called magic methods. Consider the following example, taken from OWASP:

The magic method __wakeup() is invoked after deserialization and is used to execute code that restores the object. In this example __wakeup() performs an eval using the local $hook variable. The trick to trigger an attack is to forge a serialized object that contains arbitrary code in the $hook variable. This is easily achieved as follows:

which gives:

If this value is set by the attacker in the cookies named data, then the unseralize will trigger the execution of phpinfo().

We simulate this by replacing:

with

Executing the PHP code we will see the output of phpinfo().

Unserializing arbitrary input should always be avoided. When unavoidable, it is necessary to check input integrity by adopting standard cryptographic techniques such as HMAC and digital signature.

User prepared statements for SQL queries

We have seen that generating queries by appending strings is vulnerable to SQL injections. Prepared statements offer a way to parse a parametrized query, and pass the actual parameters to the query only before it is executed. The motivation behind prepared statements is to make remote queries more efficient, by preparing the query once and only passing the missing parameters before execution. It turns out that prepared statements are very useful to prevent SQL injections: if the query has been parsed already there is no way for an attacker to inject data that might be interpreted as part of the query.

We exemplify in mysql:

If we try to perform a SQL injection through the parameters it will fail as data will only be interpreted as data (parse has already been done):

Prepared statements in PHP

Prepared statements are available in PHP through various APIs. As an example, consider again the example site which is vulnerable to SQL injection. Below, we present a version of the site using prepared statements, which is immune to SQL injection. Notice that the query is prepared with a placeholder ? for the parameter which is bound later on, before the query is executed:

PHP Data Objects (PDO)

PHP Data Objects (PDO) extension defines a lightweight, consistent interface for accessing databases in PHP. The above example cab be rewritten to work with PDO as follows. Notice that the placeholder now has a name :lastname, that is bound to the actual value later on, before execution. One of the advantages of PDO is that it support different databases through a uniform API.

Last resort: sanitization

When prepared statements are not available (for example in old applications), or when it is not possible to parametrize the query (for example the table name cannot be parametrized) it is crucial to properly sanitize the input by:

  • Typecasting for numeric parameters, for example using intval. This prevents injecting arbitrary payloads since the input is casted into an integer number.
  • Escaping string input parameters in a query, for example using mysqli_real_escape_string. Notice that escaping is not in general bullet proof. For example, mysql_real_escape_string, (without the ‘i’!) can be circumvented by exploiting different charsets and is now deprecated.

The query in the example above can be sanitized as follows:

Filter evasion

As already discussed, filtering is not bullet-proof and, when no other option is available, it is important to use standard functions to filter or sanitize. In fact, there are multitude of ways to evade filtering. Below we give a few examples of filter evasion is SQL.

A simple filter might be to forbid white-spaces. This is trivial to bypass as it is possible to separate using tabs, new lines, carriage returns or even comment symbols like /**/.

If single quotes are forbidden, it is still possible to provide hexadecimal literals to get strings since they are automatically converted depending on the context:

There are many ways to bypass filters on function names. Here is an example:

Notice that /*!50000 … executes the commented out text if the version of MySQL is greater than or equal the specified one (5.00.00 in this case). See here for more detail.