PHP remains the most popular server-side programming language. Used by over 75% of the world’s applications, PHP is now in version 8. This version, like its predecessors, always brings new features and additional layers of security.
However, PHP’s security is built on its historical features. In this article, we will review the best practices for securing your PHP applications. In particular, we will look at common vulnerabilities and attacks, as well as configuration flaws that can compromise the security of your PHP applications.
Reducing the attack surface of a web application involves minimising vulnerable entry points, by limiting the elements that can be exploited by attackers.
This includes rigorous configuration management, regular updates, error handling, source code protection and security through obscurity.
One of the most crucial aspects of security is knowing your web server’s components and configuration.
Here are some essential questions to ask yourself:
If you can’t answer these questions clearly and precisely, you’re missing information that is essential for the security of your application.
It is crucial to have a centralised, up-to-date list of the components of your infrastructure. This enables you to answer two key questions:
Security configuration must be a continuous process. This may seem obvious at the start of a project, but after several months in production, this task is often neglected.
So it’s essential to check your configurations and updates regularly to limit the risks.
Errors displayed are valuable for development. However, they can also expose sensitive information when visible in production. It is therefore essential to configure error reports appropriately.
Here are three important parameters in the php.ini file:
On a development environment, it is acceptable to display errors on screen, but in production, this must be strictly forbidden.
Instead, enable error logging and choose the types of error to be logged carefully, particularly on high-traffic sites where logs can become voluminous.
The error_reporting
parameter can be configured as follows:
error_reporting = E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT
In production, you can set display_errors
to ‘Off’ and log_errors
to ‘On’ for a better balance between security and debugging.
Find out more about configuring error reports.
Other security parameters in php.ini should be reviewed regularly, such as :
Although security through obscurity is not a complete strategy in itself, it can slow down attackers and make their task more difficult.
For example:
expose_php
in the php.ini file: expose_php = Off
.These techniques do not replace fundamental security measures, but they do make it harder for attackers to gather exploitable information.
SQL injections are dangerous attacks that can seriously compromise the security of your PHP application. Fortunately, you can protect yourself by following a few simple best practices.
The most effective way of preventing SQL injections is to use Prepared statements, available via PHP’s PDO extension.
Prepared statements separate the structure of the SQL query from the data passed to it, thus preventing the execution of malicious SQL code.
Here is an example of an implementation using PDO:
$pdo = new PDO('mysql:host=localhost;dbname=test', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = :username');
$stmt->bindParam(':username', $username);
$stmt->execute();
The parameters sent in the request are treated as data, and not as code, thus blocking any attempt at malicious injection.
Even with prepared statements, it is important to validate and sanitise user input. Use functions tailored to your needs to check the type and format of data before using it in an SQL query.
This includes using PHP filters such as filter_var()
to validate email addresses or integers.
Executing commands via PHP can be extremely powerful, but it also presents a high risk if precautions are not taken.
Poor management can allow attackers to inject malicious commands into your server, compromising the security of your application.
Here are the best practices to follow.
The first step in limiting the risk of command injection is to disable potentially dangerous PHP functions.
This can be done via the disable_functions
directive in the php.ini file. Once these functions are disabled, they cannot be exploited, even by an attacker who manages to inject PHP code via another security flaw.
Here is an example configuration:
disable_functions = show_source, exec, shell_exec, system, passthru, proc_open, popen, curl_exec, curl_multi_exec, parse_ini_file, show_source
These functions are commonly used to execute system commands, open processes or access sensitive information. Disabling them prevents misuse in the event of a breach.
If you absolutely must use command execution functions (such as exec()
or system()
), you must be extremely careful about the parameters passed to them.
Validate all input data using whitelists, i.e. accepting only pre-approved values. This reduces the likelihood of malicious input being executed as a command.
When executing commands, it is essential to escape arguments correctly to avoid injection attempts.
PHP provides native functions to secure command execution.
These functions prevent malicious users from injecting additional commands or modifying the executed command via malformed inputs.
Session hijacking is often based on exploiting session identifiers to gain access to a legitimate user’s session.
A common attack is session fixation, where the attacker forces the victim to use a session identifier known to the attacker, only to exploit that session once the victim has logged in.
To prevent this type of attack, it is essential to follow a several best practices.
The use of session identifiers in URLs makes it easier for attackers to capture or reuse this information, particularly through Man in the Middle attacks.
To avoid this, configure PHP to only accept session identifiers via cookies. This can be enabled by setting the session.use_only_cookies
parameter in the php.ini file:
session.use_only_cookies = 1
Session cookies must be protected against attacks such as cross-site scripting (XSS).
Two important measures to take :
These parameters can be activated in the php.ini file:
session.cookie_httponly = 1
session.cookie_secure = 1
The session identifiers generated must be sufficiently complex to make them difficult to predict. By increasing the entropy of identifiers, you reduce the risk of brute force attacks.
On Linux systems, we recommend using the special /dev/urandom
file to generate session identifiers with a high level of entropy:
session.entropy_file = /dev/urandom
A crucial measure to limit session fixation attacks is to regenerate the session identifier when the user performs a sensitive action, such as logging in.
This prevents an attacker from exploiting a fixed session before the user has authenticated.
PHP provides a simple function for regenerating the session ID:
session_regenerate_id(true); // true to delete the old session ID
It is advisable to regenerate the identifier at critical moments, such as :
To further strengthen the security of your sessions, several complementary strategies can be put in place:
if (!isset($_SESSION['user_agent'])) {
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
} elseif ($_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
die("User-Agent mismatch. Possible session hijacking attempt.");
}
Protecting your PHP application against XSS (Cross-Site Scripting) attacks is essential, but relatively simple if you follow rigorous guidelines.
The fundamental principle to apply is: filter inputs and escape outputs.
All user data, whether it comes from forms, URLs (GET), POST requests, cookies or even HTTP headers, must be validated and filtered before being processed by your application. This includes hidden fields and all user input, whether visible or not.
filter_var()
to ensure that the data matches your expectations (for example, an email, an integer or a URL).htmlspecialchars()
to neutralise special characters such as ‘<’, ‘>’, and ‘&’. >’ and “&”.These simple measures can already prevent most standard XSS attacks.
Additional protection against XSS attacks can be implemented via HTTP headers. Headers such as Content-Security-Policy (CSP) can limit the execution of unapproved scripts or detect potential attacks.
Content-Security-Policy lets you define which script sources are approved and authorised to run. By default, it prevents the execution of scripts injected via XSS.
Cookies should never be used to store sensitive information, as they are vulnerable to manipulation by the user or attacks such as XSS.
Here are a few best practices for using cookies securely:
Example of attributes for a cookie:
setcookie('user_session', $value, [
'expires' => time() + 3600,
'path' => '/',
'domain' => 'example.com',
'secure' => true, // Cookie sent only via HTTPS
'httponly' => true, // Inaccessible via JavaScript
'samesite' => 'Strict' // Prevents cookies being sent by cross-site requests
]);
File upload is a particularly sensitive feature. If poorly secured, it allows attackers to send malicious files to your server, including PHP scripts, which can lead to complete server takeovers.
It is therefore essential to implement a robust security strategy.
Only allow files to be uploaded by users who have been authenticated beforehand. This limits attack attempts from untrusted or anonymous users.
Only authorise file types that are strictly necessary for your functionalities (e.g. images, documents).
Set up a white list of file extensions and accepted formats, such as .jpg, .png or .pdf. Reject all other formats.
In the directory where uploaded files are stored, place an .htaccess file to prevent malicious scripts from being executed.
You can restrict access only to authorised file types:
deny from all
<Files ~ "\.(gif|jpe?g|png|pdf)$">
order deny,allow
allow from all
</Files>
This configuration prevents the execution of PHP files or any other scripts that could be downloaded maliciously.
Don’t rely solely on the file extension to validate its type. Use strict MIME type validation, checking that the file type corresponds to one of the authorised formats.
For example, to validate an image, use functions such as mime_content_type()
or getimagesize()
.
chmod()
to disable execution permissions, thus preventing any code from being executed.You can configure the maximum file size globally via the php.ini file, by adjusting the upload_max_filesize parameter.
If you want to set a stricter limit for a particular form, you can use the MAX_FILE_SIZE
parameter in your HTML form. However, this limit must always be checked on the server side, as it can be modified by the user before the form is submitted.
Example of a server-side check:
if ($_FILES['file']['size'] > 1048576) { // 1MB
die("File too large.");
}
CSRF attacks exploit the trust that a site places in an authenticated user, forcing the latter to perform unwanted actions. To protect yourself.
The first step in reducing the risk of CSRF attacks is to ensure that all actions involving state changes (such as creating, modifying or deleting data) are performed via POST requests, and not GET requests.
While this does not completely eliminate the risk of CSRF attacks, it does reduce the basic attacks where an attacker could exploit a GET request (such as a simple click on a malicious link) to trigger an action.
The most effective method of protecting against CSRF attacks is to use CSRF tokens.
These tokens are unique values generated by the server and associated with the user’s session. When a form is submitted, the server compares the token sent with the token stored in the user’s session to validate the authenticity of the request. If the tokens do not match, the action is rejected.
Here’s how to implement a simple CSRF token.
Generate a token when a form is created:
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
Add the token to the form as a hidden field:
<form method="POST" action="/submit">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
<!-- other fields -->
<button type="submit">Submit</button>
</form>
Check the token on the server side when it is submitted:
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die("Invalid CSRF token");
}
If you’re working with a framework, many modern solutions already include built-in CSRF protection.
For example, Laravel, Symfony and Django automatically generate and check CSRF tokens in forms.
If you’re not working with a framework that offers this protection natively, you can either implement the tokens yourself or use third-party libraries.
Adopting robust security practices is essential for protecting PHP applications against threats and vulnerabilities.
The various common attacks can be anticipated and reduced by systematically implementing best security practices. However, these security measures can be complex and time-consuming to implement manually in each project.
This is where modern frameworks provide an advantageous solution for developers. Integrating a framework offers a number of advantages:
Author: Amin TRAORÉ – CMO @Vaadata