Introduction
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.
Comprehensive Guide to PHP Security Best Practices
Reduce the Attack Surface 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.
Monitor PHP configurations and updates
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:
- What version of PHP is installed?
- Which libraries are used?
- What external code is included, and is it reliable?
- Which framework is used, and what version is it?
- Is the operating system up to date?
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:
- Do I really need this component? If not, it should be deleted. Any unused component represents a useless attack surface.
- How do I know if an update is available for this component? It is essential to have processes in place to track updates, via mailing lists, notifications, etc.
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.
Configure error reports appropriately
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:
- error_reporting: controls which types of error are reported.
- display_errors: defines whether errors should be displayed on screen.
- log_errors: enables errors to be logged in a log 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 :
- disable_functions: disables certain potentially dangerous functions.
- memory_limit: limits the amount of memory a script can use.
- max_execution_time: prevents scripts from running indefinitely.
- file_uploads and upload_max_filesize: control file upload authorisations and limits.
Apply the principles of security through obscurity
Although security through obscurity is not a complete strategy in itself, it can slow down attackers and make their task more difficult.
For example:
- Hide PHP version: prevents attackers from knowing the exact version of PHP being used. This can be done by disabling
expose_php
in the php.ini file:expose_php = Off
. - Generic login error messages: when a connection attempt fails, do not specify whether it is the password or the user name that is incorrect. Use a generic message such as ‘Incorrect identifiers’ to avoid revealing that a valid identifier has been found.
These techniques do not replace fundamental security measures, but they do make it harder for attackers to gather exploitable information.
Preventing SQL Injections
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.
Use Prepared Statements
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.
Validate and sanitise user input
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.
Protecting Against Command Injections
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.
Disable dangerous functions with disable_functions
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.
Strictly validate input parameters
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.
Escape commands and arguments using the appropriate functions
When executing commands, it is essential to escape arguments correctly to avoid injection attempts.
PHP provides native functions to secure command execution.
- escapeshellcmd(): Escapes special characters in a command to prevent them from being interpreted as shell operators. Use this function to secure the entire command.
- escapeshellarg(): Escapes arguments passed to a command to treat them as literal strings. This prevents the injection of special characters or additional commands.
These functions prevent malicious users from injecting additional commands or modifying the executed command via malformed inputs.
Preventing Session Hijacking
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.
Do not accept session identifiers in URLs
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
Secure session cookies with the HttpOnly and Secure flags
Session cookies must be protected against attacks such as cross-site scripting (XSS).
Two important measures to take :
- HttpOnly flag: Prevents access to cookies via JavaScript, thereby limiting the risks associated with XSS vulnerabilities.
- Secure flag: Ensures that cookies are only sent via secure connections (HTTPS).
These parameters can be activated in the php.ini file:
session.cookie_httponly = 1
session.cookie_secure = 1
Strengthen the entropy of session identifiers
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
Regenerate session identifiers during critical actions
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 :
- After the user logs in.
- When changing privileges or accessing sensitive information.
Add additional security measures (defence in depth)
To further strengthen the security of your sessions, several complementary strategies can be put in place:
- Monitor user activity: Record important actions carried out in each user’s session. For example, you can keep a record of recent interactions to detect suspicious behaviour, such as attempts to access sensitive resources without consistent prior action.
- Check the ‘User-Agent‘: Store the user’s User-Agent (browser fingerprint) in the session, then compare it when sensitive actions are taken. If the browser fingerprint suddenly changes, this could indicate a session takeover. Below is an example of implementation:
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.");
}
Countering XSS Attacks
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.
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.
- Input filtering: Validate and sanitise all user data as soon as it is received. Use PHP filters such as
filter_var()
to ensure that the data matches your expectations (for example, an email, an integer or a URL). - Escaping output: Before displaying dynamic data on your site, escape it properly to prevent the injection of malicious scripts. Use PHP functions such as
htmlspecialchars()
to neutralise special characters such as ‘<’, ‘>’, and ‘&’. >’ and “&”.
These simple measures can already prevent most standard XSS attacks.
Strengthening protection with HTTP headers
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 protection
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:
- Don’t store sensitive information: Cookies are not inherently secure. Avoid using them for confidential or critical data.
- Sign cookies: To protect cookies against manipulation, an effective method is to sign them by calculating a hash that you store in the cookie itself. When the cookie is sent back to the server, the latter can check its integrity by recalculating the hash.
- Use security attributes: As indicated above for session hijacking, activate the HttpOnly and Secure attributes for your cookies.
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
]);
Securing File Uploads
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.
Restrict access to authenticated users
Only allow files to be uploaded by users who have been authenticated beforehand. This limits attack attempts from untrusted or anonymous users.
Restrict the extensions and formats accepted
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.
Configure an .htaccess file to restrict access to uploaded files
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.
Validate the MIME type and file extension
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()
.
Manage files securely when saving them
- Generate unique file names: Avoid using file names provided by users.
- Store files outside the root directory: To prevent malicious files from being accidentally executed, store uploaded files outside your site’s root directory (for example, in a dedicated directory outside the public ‘www’ or ‘htdocs’ directory).
- Revoke execution permissions: After saving the file, use
chmod()
to disable execution permissions, thus preventing any code from being executed.
Limit file size with upload_max_filesize
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.");
}
Protecting Against CSRF (Cross Site Request Forgery) Attacks
CSRF attacks exploit the trust that a site places in an authenticated user, forcing the latter to perform unwanted actions. To protect yourself.
Use POST requests for sensitive actions
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.
Implement CSRF tokens in forms
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");
}
Use anti-CSRF libraries or frameworks
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.
Conclusion
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:
- Built-in security features: Modern frameworks often offer built-in protection against common vulnerabilities. For example, the use of prepared statements to counter SQL injections, automatic input filtering to prevent XSS attacks, or the management of anti-CSRF tokens.
- Simplified maintenance: The frameworks benefit from an active community of developers who provide regular updates, correcting identified vulnerabilities and ensuring continuous improvement of security standards. This considerably reduces the risk of exploits from obsolete components.
- Saving time and reducing costs: By automating many aspects of security and offering ready-to-use libraries, frameworks enable teams to concentrate on business-specific issues, without having to develop security solutions from scratch. This time saving also translates into optimised development resources and costs.
- Consolidated best practice: The frameworks are designed around proven and tested principles. They incorporate solid architectures that promote modular and secure development, limiting human error and facilitating long-term code maintenance.
Author: Amin TRAORÉ – CMO @Vaadata