The golden rule of security:
never trust user input
Literally virtually all software exploits are a result of input validation or sanitization failure.
There is a security notes thread in the FAQ section. It is old, but mostly still relevant. Don't read past the first post as most of the comments in it are garbage. I am in the process of rewriting it but have not finished yet and will probably not be finished for several months.
However, here is what I've completed so far on the rewrite. Obviously there are various formatting errors and incomplete sections and probably a few errors, but the information is still solid:
This thread is a consolidated, reorganized, updated and expanded version of the previous security notes thread originally written by JeffCT in 2001.
The purpose of this article is to document the most common security-related coding mistakes made by PHP programmers. Following these security practices will eliminate the vast majority of security problems with your PHP code. Many of these security holes have been found in widely-used open source and commercial PHP scripts in the past. Most PHP scripts are compromised because the script does not validate or sanitize user input; user-supplied input must never be trusted in any circumstances.
This thread is not to be used for general discussion. If you have a question or comment on PHP security please create a new thread. You may reply to this thread, however all replies will either be split into separate topics or deleted once addressed (for example, if the reply points out a mistake in the original thread). Separate threads which explore certain aspects of PHP security in more depth may be referenced at the end of this article.
One final word of warning: this thread contains some code examples that are intentionally insecure; do not copy and paste code these examples out of this thread!
Section 1: Validate, sanitize and never trust user-supplied data
Chapter 1.1: Protect against SQL injection
For an in-depth explanation of SQL injection, please consult the Wikipedia article on it:
SQL Injection. In a nutshell, a SQL injection exploit exists whenever you use user-supplied data as part of a SQL query without sanitizing it.
For example, the following is a very common example of code that is vulnerable to a SQL injection attack:
PHP Code:
$result = mysql_query("SELECT * FROM users WHERE username = '" . $_POST['username'] . "' AND password = '" . $_POST['password'] . "'");
if(mysql_num_rows($result) > 0) {
// User login successful
}
The SQL injection vulnerability in this code would allow me to authenticate as any user without needing to know their password. For a non-malicious user, the SQL query executed against the database would look something like:
Code:
SELECT * FROM users WHERE username = 'user1' AND password = 'pass1'
However, a malicious user could enter their username as admin and their password as:
Code:
' OR username = 'admin
And the resulting query that gets executed becomes:
Code:
SELECT * FROM users WHERE username = 'admin' AND password = '' OR username = 'admin'
When this query is executed, the row for the admin user will be returned
even though the password is wrong, and the user will have successfully logged in as the admin user without needing to know the admin password.
Protecting against SQL injection is very simple, but differs depending on which database library you use.
For the deprecated mysql library, use mysql_real_escape_string
PHP Code:
$result = mysql_query("SELECT * FROM users WHERE username = '" . mysql_real_escape_string($_POST['username']) . "' AND password = '" . mysql_real_escape_string($_POST['password']) . "'");
if(mysql_num_rows($result) > 0) {
// User login successful
}
For the mysqli procedural library, use mysqli_real_escape_string
PHP Code:
$result = mysqli_query("SELECT * FROM users WHERE username = '" . mysqli_real_escape_string($_POST['username']) . "' AND password = '" . mysqli_real_escape_string($_POST['password']) . "'");
if(mysqli_num_rows($result) > 0) {
// User login successful
}
For the mysqli object oriented library, use real_escape_string
PHP Code:
$result = $mysqli->query("SELECT * FROM users WHERE username = '" . $mysqli->real_escape_string($_POST['username']) . "' AND password = '" . $mysqli->real_escape_string($_POST['password']) . "'");
if($result->num_rows > 0) {
// User login successful
}
For the PDO library, use quote
PHP Code:
$result = $pdo->query("SELECT * FROM users WHERE username = '" . $pdo->quote($_POST['username']) . "' AND password = '" . $pdo->quote($_POST['password']) . "'");
if($result->rowCount() > 0) {
// User login successful
}
For mysqli and PDO, it is also possible to use prepared statements to protect against SQL injection:
PHP Code:
$st = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
$result = $st->execute(array(':username' => $_POST['username'], ':password' => $_POST['password']));
if($result->rowCount() > 0) {
// User login successful
}
Special care must be taken when using user-supplied numeric parameters. For example, consider the following:
PHP Code:
$result = mysql_query("SELECT * FROM users WHERE username != 'admin' AND id = " . mysql_real_escape_string($_POST['userid']));
This code is not supposed to allow the admin user to be retrieved, however, despite the user of mysql_real_escape_string, this code is still vulnerable to SQL injection. For example, consider passing the following value in userid
The resulting query is:
Code:
SELECT * FROM users WHERE username != 'admin' AND id = 1 OR id = 1
When this query is run, user ID 1 will be selected even if that user is the admin user.
To properly handle integer parameters you must do one of the following:
* Enclose the value in quotation marks in the query even though it is numeric
* Cast the value to an integer instead of using an escape/quote function
* Validate that the parameter is numeric and do not run the query if it is not
* Use prepared statements (for mysqli or PDO)
Chapter 1.2 - Protect against XSS attacks
For an in-depth explanation of XSS attacks, please consult the Wikipedia article on it:
Cross-site scripting. In a nutshell, an XSS exploit exists whenever you use user-supplied data as part of the output you send back to the user. XSS vulnerabilities frequently allow attackers to execute arbitrary JavaScript code on your page, which enables them to hijack your users' sessions and redirect them to malicious pages.
The following is a very common example of code that is vulernable to an XSS vulnerability:
PHP Code:
<?php ?>
<input type="text" name="username" value="<?php echo $_GET['username']; ?>" />
An attacker could link an innocent user to this page with the username parameter set to:
Code:
" onclick="window.location='http://malicioussite.com/'
And the resulting HTML that is sent to the innocent user becomes:
Code:
<input type="text" name="username" value="" onclick="window.location='http://malicioussite.com/'" />
As soon as the user clicks on the form field, they will e redirected to the malicious site controlled by the attacker.
To protect against XSS attacks, any output used in HTML must be cleaned using a function like htmlentities:
PHP Code:
<?php ?>
<input type="text" name="username" value="<?php echo htmlentities($_GET['username'], ENT_QUOTES, 'UTF-8'); ?>" />
Another very common security flaw is the use of $_SERVER['PHP_SELF'] as the action attribute value of a <form> tag. For example:
PHP Code:
<?php ?>
<form action="<?php echo $_SERVER['PHP_SELF']; ?>">
htmlentities is also used to fix this vulnerability:
PHP Code:
<?php ?>
<form action="<?php echo htmlentities($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8'); ?>">
When outputting values into JavaScript you should use json_encode instead. For example:
PHP Code:
<?php ?>
<script type="text/javascript">
var username = '<?php echo $username; ?>';
</script>
Should be written as:
PHP Code:
<?php ?>
<script type="text/javascript">
var username = <?php echo json_encode((string)$username); ?>;
</script>
Chapter 1.3: Protect against file path manipulation
Using improperly sanitized user-supplied data as part of a file path can lead to vulnerabilities that allow attackers to access arbitrary files on the server. This is known as a remote file inclusion vulnerability.
Here is an example of vulnerable code:
PHP Code:
<?php
require("pages/{$_GET['page']}.php");
An attacker could send a page parameter with a value like "../../../../etc/passwd\0" and steal a list of user accounts from your server. There are two key elements to this attack: the first is the use of "../" to go up a directory level. This allows the attacker to include files that are outside of your web root. The second is the use of a null-byte "\0" at the end of the filename. In C programming, a null-byte is used to designate the end of a string. This allows an attacker to discard and ignore the rest of the string (".php"), thus letting them to point to any arbitrary file regardless of its extension.
This same potential for exploitation exists for all function that use file system paths (file_get_contents, fopen, move_uploaded_file, etc.).
Chapter 1.4: Protect against malicious file uploads
An improperly validated file upload script will attacks to take complete control of your server by uploading and executing arbitrary PHP code. This type of attack is devestating to security, and the only proper resolution if it does happen is to wipe your server and restore from an uncompromised backup of the code.
All of the issues inherent with using user input in file paths also exists with file uploads, but there are additional security concerns related to the type, extension and contents of the uploaded file.
Here is an example of insecure upload code:
PHP Code:
$uploads_dir = '/uploads/';
$tmp_name = $_FILES['picture']['tmp_name'];
$name = $_FILES['picture']['name'];
move_uploaded_file($tmp_name, $uploads_dir . $name);
Here is another example of insecure code:
PHP Code:
$uploads_dir = '/uploads/';
$uploads_ext = '.jpeg';
$tmp_name = $_FILES['picture']['tmp_name'];
$name = pathinfo($_FILES['picture']['name'], PATHINFO_BASENAME);
move_uploaded_file($tmp_name, $uploads_dir . $name . $uploads_ext);
And here is a third example of insecure code:
PHP Code:
$uploads_dir = '/uploads/';
$is_image = strpos($_FILES["picture"]["type"], "image");
$tmp_name = $_FILES['picture']['tmp_name'];
$name = $_FILES['picture']['name'];
if($is_image !== FALSE) {
move_uploaded_file($tmp_name, $uploads_dir . $name);
}
And yet a fourth example of insecure code (there are a lot of ways of doing this wrong):
PHP Code:
$uploads_dir = '/uploads/';
$tmp_name = $_FILES['picture']['tmp_name'];
$name = $_FILES['picture']['name'];
if(mime_content_type($tmp_name) === 'image/png') {
move_uploaded_file($tmp_name, $uploads_dir . $name);
}
When allowing a user to upload a file you must never allow the user to arbitrarily specify the extension of the uploaded file. Always validate the extension of the file name against a white list of valid file extensions.
And finally, here is an example of the only secure way to perform file upload validation - a file extension check:
PHP Code:
$uploads_dir = '/uploads/';
$tmp_name = $_FILES['picture']['tmp_name'];
$name = pathinfo($_FILES['picture']['name'], PATHINFO_BASENAME);
$allowed_extensions = array('png');
if(in_array(pathinfo($name, PATHINFO_EXTENSION), $allowed_extensions)) {
move_uploaded_file($tmp_name, $uploads_dir . $name);
}
Alternatively: do not allow the user to specify file name at all.
Chapter 1.6 - Do not trust user-supplied form values
Select boxes do not force a user to select a valid option. A malicious user can submit any value, as if the select box were a text input. When a select box is submitted, the option must be validated against the list of items that were originally present in the box.
The same applies to hidden input fields. It is trivial for a malicious user to change the value of a hidden field, so they must never be used to store anything that you don't want the user to be able to change (for example, a price).
Chapter 1.7 - Do not trust HTTP_REFERER
$_SERVER['HTTP_REFERER'] cannot be used for security purposes because it can be arbitrarily changed by the user with no effort. Additionally, some legitimate users will not send valid HTTP_REFERER headers, and they will not be able to use any section of your site that depends on HTTP_REFERER being set.
There is no reliable way to obtain the address of the referring page.
Chapter 1.8 - Do not trust cookies
The value of a cookie can be arbitrarily changed by the user with no effort. Do not use them to store information that you do not want the user to be able to change (such as the user ID that they are logged in as).
Additionally, passwords, even hashed passwords, should never be stored in a cookie.
If you need to track data for a visitor and do not want them to be able to change it, you must use a session instead.
Chapter 1.9 - Validate user-supplied redirect URLs
Whenever using a user-supplied URL for redirection you need to validate the URL to ensure that it belongs to your website. Not doing so allows an attacker to abuse your redirection feature for phishing.
User-supplied redirection URLs are commonly used on login forms. For example, if a user visits /my-account and they are not logged in, your site might redirect them to /login.php?after-login-redirect-to=/my-account.
After the user logs in, your application may perform a simple redirect ack to the /my-account page:
PHP Code:
<?php
header("Location: {$_GET['after-login-redirect-to']}");
exit;
However, an attacker could send a novice user a URL like: http://yoursite.com/login.php?after-login-redirect-to=http://malicioussite.com/
The user will see the page as being from your trusted site, and the login page they receive will be on your domain and show your security certificates; but after they log in they are automatically sent to a malicious site.
To avoid this problem, validate the URL before performing the redirect. This may be as simple as not allowing absolute URLs, or you may need to validate the domain name against a white list of valid redirect domains.
Chapter 1.4 - Protect against PHP injection
Use of the eval function is usually a bad idea, but using user-supplied data as part of a string passed to eval is a worse idea. The best approach is to not do this at all, but if you must, make sure the user-supplied data is properly validated and sanitized.
There is no general-case solution to sanitizing user-supplied data used in eval; it depends completely on how the data is used within eval.
Chapter 1.?: Protecting against shell injection
When using shell functions like system, exec, passthru, etc. any user-supplied data passed to the functions must be escaped using escapeshellarg similar to the way mysql_real_escape_string would be used.
Chapter 1.10 - Protect against CSRF
Chapter 1.11 - Things to NOT do when escaping data
Section 2: Storing user data
Chapter 1.1 Storing passwords
Chapter 1.2 Storing credit card data
Simple: don't do it. Unless you already know enough to have written this guide, you do not know enough about security to store credit card information. Use a hosted API or hire an expert.
I would also like to point out that it is a violation of every merchant agreement to ever store CVV/CCV/card security codes in any format for any length of time. Storing these may result in the revocation of your company/client's ability to process credit cards and/or fines of up to $500,000.
Section 3: Application design
Chapter 3.1 Always re-validate user authentication on private pages
Chapter 3.2 Call exit or die after redirecting a user
A header redirect like the following is often used to redirect the user:
PHP Code:
<?php
header("Location: http://url/");
However, it is important to note that PHP does not stop executing after the call to header(). The rest of the script still executes, and all of its output is still send to the user. There are two related security holes that might result from this:
PHP Code:
<?php
if(!$user_has_access) {
header("Location: /access-denied.php");
}
some_protected_database_change();
echo "some protected content;
In this example, the PHP script will still execute the function some_protected_database_change() even if the user does not have access to the page (ie: $user_has_access is false). Additionally, "some protected content" will still be sent to the user's browser so they can access this if they want to see it.
Section 4: PHP configuration
Chapter 4.1 - Handling error reporting correctly
On a production web server, display_errors should
never be enabled. PHP error messages can provide attackers with information about your server, such as file paths, file names, and even database connection usernames and passwords.
On a production web server, log_errors should always be enabled and error_log should be set. You should monitor these, as error messages can often tell you when someone is attempting to break into your server.
On both development and production machines, error_reporting should always be set to E_ALL.
All of these configuration parameters can be set using the ini_set function, but ideally they should be set in php.ini instead.
Chapter 4.2 - Do not rely on insecure PHP features