PHP Security Guide


Since PHP is a very high-level scripting language, some of the potential security flaws that many languages present are totally irrelevant to it: you don’t have to manage your memory, for example, and thus don’t have to worry about things like buffer overflows. Being a high-level language also means that PHP is much easier to learn; however, this is possibly the biggest problem with the language as a whole. Many people learn PHP as a first language and don’t consider some of the fundamental considerations one must make when writing programs, particularly ones as public as are most web applications.

This guide aims to familiarise you with some of the basic concepts of online security and teach you how to write more secure PHP scripts. It’s aimed squarely at beginners, but I hope that it still has something to offer more advanced users. Enjoy.

Different Types of Attack


XSS stands for “Cross Site Scripting”, and refers to the act of inserting content, such as Javascript, into a page. Usually these attacks are used to steal cookies which often contain sensitive data such as login information.

An example of a script vulnerable to XSS is this simple script to fetch a news item based on its ID:

$id = $_GET['id'];
echo 'Displaying news item number '.$id;
/* snip */

Now, if $_GET['id'] contains a number, then all’s well and good—but what happens if it contains this?

window.location.href = "" + document.cookie;

If a user passed this simple Javascript into the $_GET['id'] variable and convinced a user to click it, then the script would be executed and pass the user’s cookie data onto the attacker, allowing them to log in as the user. It’s really that simple.

Firstly, you must never implicitly trust user input. Always presume that every bit of input contains an attack, and code to account for that. To do this, you need to filter user input, removing it of HTML tags so that no Javascript can be run. The easiest way to do this is with PHP’s built in strip_tags() function, which will remove HTML from a string rendering it harmless. If you just want to make the HTML safe without removing it altogether, then you need to run the input through htmlentities(), which will convert < and > to &lt; and &gt; respectively.

SQL Injection

Many sites use databases as a backend to store their data, using queries to insert and select data from it. However, many people are unaware that such sites are often vulnerable to a form of attack called SQL injection.

SQL injection is when malformed user input is used directly and deliberately in an SQL query, in a way that allows the attacker to manipulate the query. This means that an attacker could delete portions of your database, make himself an admin account etc—the possibilities are endless.

One of the most common vulnerabilities is when logging in to a site. Take this example:

$username = $_POST['username'];
$password = $_POST['password'];
$result = mysql_query("
    username = '$username'
    password = '$password'
if ( mysql_num_rows($result) > 0 )
    // logged in

This is vulnerably to a pretty obvious SQL injection; can you work out how an attacker could modify the query to allow himself to be logged in regardless of whether or not he has the right password?

If the attacker enters a valid username in the username field—”rob”, say—and the following in the password field:

' OR 1=1 '

The resulting query will look like this:

    username = 'rob'
    password = '' OR 1=1

It will therefore select all users where:

  • the username is “rob”
  • either the “password” field is empty, or 1 is equal to 1

Since the last criteria will always be true—when is 1 ever not equal to 1?—the user will be able to log in as rob without knowing rob’s password. Eek!

As with XSS attacks, you must never trust user input. The best way of cleaning user input is using PHP’s built in mysql_real_escape_string() function; this will escape characters such as ', " and others, making them useless in “breaking out” of a quoted string as in the above example. If you’re using a number in your query, then you should use intval() on the inputted number to ensure it is numeric.

Spoofed Form Input

As an extension of the above two points, it’s important to remember that input sent to your script may not have been sent from the form you created. This means that, although you might have data in checkboxes, radio buttons, selects or other “read-only” elements, they might contain values that were never in the elements you created and thus need filtering just like inputs and textareas.

This also means that you cannot rely soley on client-side validation. Whilst it’s nice to have errors pointed out to the user without having to reload a page (and possibly lose all of their input), using client-side validation as a security measure is not sensible. Make sure you check all input server-side, in your PHP scripts, before you do anything like insert it into a database.

See the CSRF section below for more details on how to avoid spoofed form input.


CSRF stands for “Cross-Site Request Forgery”, and CSRF attacks are similar in scope and methodology to XSS attacks. CSRF attacks usually either exploit the fact that many websites perform actions on HTTP GET requests—deleting blog posts, buying items etc.—or spoof a client request to a resource so that the website believes the request is genuine. Either way, the victim performs an action on a website that trusts him—usually his own—that he did not intend to happen.

First, we’ll begin with an example attack, then we’ll look at some ways of defending against such attacks.

Many websites allow you to perform actions at the click of a link, such as deleting a forum post. Usually, the URLs that perform these sorts of actions look a little like this:

deletepost.php will typically check that the user performing the request is logged in, and if so perform the requested action—in this case, deleting the post with the id of 392. However, this method of authentication leaves open a massive security flaw; what if a privileged user—a forum moderator, for example—were to be tricked or forced into visiting this URL? The post would be deleted, but that’s not what the moderator wanted. An attacker could even go further—if the URL were entered in an HTML <img> tag, for example, the privileged user would likely not even know that they had performed the action.

How, then, can we avoid such attacks? There are two methods that, when used together, completely eliminate the possibility of CSRF attacks.

The first is rather simple: never, ever use GET for any critical task. Instead, use a POST form. Such requests are harder to forge and have the added bonus that they are impossible to load into HTML image/script tags, eliminating an attacker’s ability to exploit your site remotely.

The second is to make sure all requests originate from your own forms, eliminating the possibility that the request could have been loaded from a fake form on a different webpage. To do this, we can create a value— known by some as a “nonce”, but here referred to as a “token”—that is created especially for the form, submitted along with it, and checked— along with the usual permission checks—before the action is performed.

Here’s an example that creates and checks a token before deleting a forum post:

if( !empty($_POST['post_id'] ) {
    if( !user->is_a_moderator )
    if( empty($_POST['token']) || $_POST['token'] != $_SESSION['token'] )
    // All fine: delete the post.
    delete_post( intval($_POST['post_id']) );
    // Unset the token, so that it cannot be used again.
$token = md5(uniqid(rand(), true));
$_SESSION['token'] = $token;
<form method="post">
<p>Post ID to delete:</p>
<p><input type="text" name="post_id" /></p>
<input type="hidden" name="token" value="<?php echo $token; ?>" />

As we can see, using a POST form with a generated token is simple, straightforward and eliminates the possibility of CSRF attacks.

File Uploads

File uploads are potentially the biggest security risk in web development. Allowing a third-party to place files on your server could allow them to delete your files, empty your database, gain user details and much more.

However, it’s certainly possible to upload files safely, and such functionality can be a great feature of your site.

When allowing users to upload files from their local machine to your server, there are two things that you need to check. The first is the mime-type of the uploaded file; if your script is uploading images, for example, you’ll want to just accept image/png, image/jpeg, image/gif, image/x-png and image/p-jpeg. You can do so as follows:

$validMimes = array(
$image = $_FILES['image'];
if(!in_array($image['type'], $validMimes)) {
    die('Sorry, but the file type you tried to upload is invalid; only images are allowed.');
// Do something with the uploaded file.

The second thing to check is the file extension. It’s certainly possible to spoof a mime-type; one vector is to take an image, insert PHP code into the sections the file format allows for meta data, give it a .php extension, and upload it. In this case, your mime-checking would think the file was an image, upload it, and allow execution of the PHP code within.

To avoid this, you should manually assign files an extension based on their mime-type. We could extend our above example to take this into account:

$validMimes = array(
    'image/png' => '.png',
    'image/x-png' => '.png',
    'image/gif' => '.gif',
    'image/jpeg' => '.jpg',
    'image/pjpeg' => '.jpg'
$image = $_FILES['image'];
if(!array_key_exists($image['type'], $validMimes)) {
    die('Sorry, but the file type you tried to upload is invalid; only images are allowed.');
// Get the filename minus the file extension:
$filename = substr($image['name'], 0, strrpos($image['name'], '.'));
// Append the appropriate extension
$filename .= $validMimes[$image['type']];
// Do something with the uploaded file

You can see how the above attack is avoided; if the image containing the PHP code was called foo.php and was a PNG, it would would be renamed to foo.png and the code would not be executed.

Including Files

Never, ever include files based on user input without thoroughly checking said input first. One of the major culprits of this is the ubiquitous index.php?page=something.php script that so many people love to use:

include $_GET['page'];

By doing so, you can make maintenance of your site much easier; you can keep content in individual files, and make changes to areas such as the navigation in just one file and have them appear globally—much like frames, but without the client-side disadvantages they bring. However, there is a problem with this method; it allows the user to specify whatever file they like, giving them access to the contents of any file on the server that your PHP script has permission to open. Even worse, if the PHP directive allow_url_fopen is turned on, an attacker can open files from another server—and execute any PHP code within them, since the script uses include and not something like echo file_get_contents(). This gives them complete control of your webserver and the files on it. As you can see, this is very bad.

You can prevent this in one of two ways. If you only have a few pages, you can make a white-list of pages that are allowed, like so:

switch($_GET['page']) {
    case "about":
    case "news":

This method means that only the pages you explicitly and specifically allow can be included into the page, removing any possibility of an attack. However, it’s rather cumbersome—every time you add a new page, you have to edit this file and add it to the whitelist.

So, a better method would be to simply clean the input to make sure that it’s safe. This strikes a balance between the ease-of-use of the first method and the security of the second.

$page = preg_replace('/\W/si', '', $_GET['page']);

In this particular script, we take two steps to make sure that the file is valid. The first, scary-looking line is a regular expression, which removes all non-word characters—that is, non alpha-numeric ones—from the input. This means that an attacker cannot traverse directories using .., or input a URL—, for example, would be filtered to httpwwwgooglecom: useless, but safe.

Another related point is the naming of included files. Many scripts store their settings in external files to make it easy for end-users to change them. If you’re working on a script that does this, be sure to name your included files with an extension that isn’t displayed as plain text. Many scripts use .inc, which by default is displayed as a regular text file in most web servers. This could give users access to sensitive information such as database details and user info. The best option is to name the files with an extension of PHP; that way, if a user requests the files, they’ll simply be greeted with a blank page.

If you’re using Apache, and using a script that insists on using INC files, then you can use this setting to disallow direct access to .inc files:

<files ~ "\.inc$">
Order allow,deny
Deny from all

This should be placed in a file called .htaccess, in your top-level directory. It basically disallows end-users from viewing .inc files, but still allows scripts to include and use them.


eval() is a useful but very dangerous function that allows you to execute a string as PHP code. There aren’t many occasions where this is neccessary, and being realistic you should avoid its usage, especially if you want to use user input in the string.

Register Globals

register_globals is a PHP setting that automatically takes data from the superglobal arrays ($_GET, $_POST, $_SERVER, $_COOKIE, $_REQUEST and $_FILE) and assigns them to global variables; $_POST['message'] would automatically be assigned to $message, for example. This setting is automatically disabled with new installations of PHP, and with good reason. Take this example:

if($_POST['username'] == 'rob' && $_POST['password'] == 'foo') {
    $authenticated = true;
if($authenticated) {
    // do some admin thing

Now, with register_globals turned off, this script works as intended; $authenticated is only set if the user has entered the correct password. However, with register_globals turned on, a malicious user could run the script as


and he would automatically be granted admin rights.

There’s not a whole lot you can do about this setting if you’re using shared hosting, but you can code your scripts so that they aren’t affected by any malicious exploitation of register_globals. The above example, for instance, would become:

$authenticated = false;
if($_POST['username'] == 'rob' && $_POST['password'] == 'foo') {
    $authenticated = true;
if($authenticated) {
    // do some admin thing

By explicitly setting $authenticated to false, we avoid any potential overrides through register_globals.

Magic Quotes

Magic Quotes were an attempt by the PHP developers to add some default security into PHP; when the magic_quotes_gpc setting is turned on, all ‘ (single quote), ” (double quote), \ (backslash) and NULL characters are escaped with a backslash automatically. Note that this is NOT the same as mysql_real_escape_string(), and by turning it on you do NOT prevent all SQL injection attacks. This is the first problem.

The second is that that they pose a portability nightmare. Some hosts have the setting on, and others don’t; if you’re writing a script that’s going to be used on multiple systems, you need to check whether magic quotes is turned on and act appropriately.

However, there are solutions to this. One method is to check if the setting is turned on and, if it isn’t, add magic quotes yourself:

function add_magic_quotes($array) {
    foreach ($array as $k => $v) {
        if (is_array($v)) {
            $array[$k] = add_magic_quotes($v);
        } else {
            $array[$k] = addslashes($v);
    return $array;
if (!get_magic_quotes_gpc()) {
    $_GET    = add_magic_quotes($_GET);
    $_POST   = add_magic_quotes($_POST);
    $_COOKIE = add_magic_quotes($_COOKIE);

Alternatively, you can do the opposite, and remove the slashes if magic quotes are turned on:

function remove_magic_quotes($array) {
    foreach ($array as $k => $v) {
        if (is_array($v)) {
            $array[$k] = remove_magic_quotes($v);
        } else {
            $array[$k] = stripslashes($v);
    return $array;
if (get_magic_quotes_gpc()) {
    $_GET    = remove_magic_quotes($_GET);
    $_POST   = remove_magic_quotes($_POST);
    $_COOKIE = remove_magic_quotes($_COOKIE);

Include either of these methods at the top of your main include, and rest easy.

Error Reporting

If you have error reporting turned on fully, important information can be displayed in the event of an error—even a relatively minor one. PHP provides a function called error_reporting() that allows you to change the level of error reporting on a per-script basis.

Whilst in development, you should take advantage of this function to display all errors, warnings and notices, like so:

error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE);

This helps to avoid any errors appearing on production sites that you’d missed in development, and helps you produce better code—especially since notices are usually about flaws in style.

However, when you put your site into production, this level of detail can be dangerous. You can’t forsee all errors during development—your program could run out of memory or diskspace, for example. So, for safety’s sake, on production sites you should disable the displaying of errors and instead log them to a file safely outside of your directory root; this way, the public can’t see if anything goes wrong, but you can. Here’s a simple bit of code that will accomplish this:

error_reporting(E_ALL^E_NOTICE); // This is a 'sensible' reporting level
ini_set('display_errors', 0); // Hide all error messages from  the public
ini_set('log_errors', 1);
ini_set('error_log', 'path/to_your/log.txt'); // Preferably a location outside of your web root

Be sure to edit the path to the error log so that it’s a correct path and one that is writeable by the server process—and check regularly for errors.

PHP also offers a function, set_error_handler(), that allows you to write your own error handling function that is called when an error occurs. This way, you could implement a much more sophisticated system, perhaps displaying errors to admins or giving a “nice” message to users when the site is down, rather than a confusing PHP-generated error or even a blank page.

Another important thing that people sometimes miss is mySQL error reporting. A useful tip for development is to print error reports when a query fails so you can see what went wrong, like so:

FROM table_that_doesnt_exist
') or die(mysql_error());

During development, this is great—it allows you to see quickly that the table doesn’t exist, and that’s why it’s breaking. However, you should never leave this on in a production environment; if there happens to be an error, you should log the error appropriately and give a generic error message to the user if it’s critical. If you don’t, an attacker can find out important information about your database schema and even some login information.


PHP 5 offers error reporting that is worlds away from the simple system in PHP 4. One of the most important additions is the ability to throw and catch exceptions, a feature that many languages have offered for years but PHP has only just picked up.

When a function encounters an exceptional circumstance it can raise—or throw—an exception. The code executing that function can then try the function, catch any exceptions that occur and act accordingly. If the exception is not caught, your script will display a fatal error and halt execution.

Here’s some example code that should make things a bit clearer for you:

function getData($filename) {
    if(!file_exists($filename)) {
        throw new Exception('File does not exist');
    if(filesize($filename) == 0) {
        throw new Exception('File is empty');
    // If any of the above exceptions are thrown,
    // this code will not be executed:
    return file_get_contents($filename);
try {
catch(Exception $e) {
    echo 'There was an error opening the file: '.$e->getMessage();

You may also extend the exception class: the PHP manual has some great information on the subject.

As you can see, exceptions are a great way of avoiding the display of nasty PHP errors and maintaing the integrity of your code, especially when you do a little more with the catch section than simply display a message. You can keep your code running even if a severe error occurs, and by extending Exception you can make the source of errors clear to other developers.

However, exceptions are quite performance intensive and not always useful to the end user; use them sparingly and know when to use default error handling instead.

Plain Text Passwords

When storing passwords, it’s important never to store them in plain text. The reasoning behind this is that, if an attacker were ever to gain access to your database or to a user’s cookies, they would know the user’s password which they could potentially use on many other sites—as well as the fact that they would be able to log in as that user on your site.

How, then, can you hide the user’s actual password whilst also retaining the ability to check if a user’s password is correct when logging in? The answer is something called “hashing”. This way, you store a hash of the user’s password in the database, and hash the input when logging a user in; if the hashes match, the input is correct. This way, you never have to store the user’s actual password.

There are two main methods of hashing: MD5 and SHA1. PHP offers functions for both; SHA1 is generally regarded as stronger by professional cryptologists, but both are perfectly adequate for most people’s needs.

Here’s a quick example of a safe way to do things:

$user_name = mysql_real_escape_string($_POST['username']);
$user_password = md5($_POST['password']);
$result = mysql_query('
FROM users
WHERE user_name = "'.$user_name.'"
AND user_password = "'.$user_password.'"
$row = mysql_fetch_assoc($result);
if($row['count'] > 0) {
    // Password is okay.

Also worth noting is that there’s no need to escape the MD5d password before inserting it into the database, since MD5 hashes are always alphanumeric.

Taking it further: Salting

Salting refers to the concept of adding an extra piece of information to the data we’re hashing before we hash it. This means that even if a user were to have a list of every known MD5/SHA1 hash (which is an absolute impossibility, naturally), plus the user’s password hash, they’d still not be able to derive the password from the hash by comparison unless they also knew the salt data, making dictionary attacks useless.

This is perhaps better explained with an example.

Let’s say a particularly unimaginitive user chooses for their password the word “password”. If we don’t implement salting, the password is stored in the database as 5f4dcc3b5aa765d61d8327deb882cf99, the plain MD5 value of “password”.

If a hacker gains access to the password hash of a user, either from their cookie or from the database itself, they could then scan through a “dictionary” of words, hashing each one with MD5, until they found a hash that matched the user’s—they would then know that that particular word was the user’s password.

By introducing salting, however, we’d end up hashing the salt as well as the password, and storing that in the database. Given a relatively strong salt of “f963kjg”, our hash would become b9be01b2cf3d77fe60f6e8892d664606, and the attacker would never be able to gain access to the user’s password—not unless they had the string “f963kjgpassword” in their dictionary, anyway.

So, to use a salted hash, we simply hash the salt and the password together:

$salt = 'thequickbrownfox';
$password 'foobar123';
$salted_hash = md5($salt . $password);

And do the same when checking the password:

$salt = 'thequickbrownfox';
$user_name = mysql_real_escape_string($_POST['username']);
$user_password = md5($salt . $_POST['password']);
$result = mysql_query('
FROM users
WHERE user_name = "'.$user_name.'"
AND user_password = "'.$user_password.'"
$row = mysql_fetch_assoc($result);
if($row['count'] > 0) {
    // Password is okay.

Considering how simple a change salting is compared with the security benefits it brings, it’s certainly worth doing.


Hopefully this has made you a little more aware of the dangers that can face you when writing PHP scripts, and hopefully you’ve understood what I’ve tried to say. Remember: always presume that input is malformed, act accordingly, and you’ll be fine.

Any comments, questions or suggestions? Please email me at

Enjoy this guide/find it useful? Please consider Rz Rasel!

By Rz Rasel Posted in Php

39 comments on “PHP Security Guide

  1. Pretty section of content. I just stumbled upon your website and in accession capital to assert that I get actually enjoyed account your blog posts.
    Any way I will be subscribing to your feeds and even I achievement you access consistently rapidly.

  2. Soon after study some with the websites along with your internet internet site now, i genuinely as if your way of blogging. I bookmarked it to my bookmark internet site list and is going to be checking back soon. Pls appear at my internet site likewise and figure out what you believe. 760058

  3. I am really impressed with your writing skills and also with the layout on your weblog.
    Is this a paid theme or did you modify it yourself?
    Either way keep up the nice quality writing, it’s rare to see a nice blog like this one nowadays.

  4. Pingback: Homepage

  5. Hello there, just became alert to your blog via Google, and discovered that its truly informative. Im gonna watch out for brussels. I will appreciate in the event you continue this in future. Lots of individuals will probably be benefited from your writing. Cheers! xrumer 302731

  6. Hello there, I discovered your blog by way of Google at the same time as looking for a comparable topic,
    your web site came up, it looks good. I have bookmarked it in my
    google bookmarks.
    Hello there, just became aware of your blog thru Google, and located that it’s truly informative. I am gonna be careful for brussels. I will appreciate when you proceed this in future. Lots of other people will be benefited out of your writing. Cheers!

  7. I observe there is actually a lot of spam on this weblog. Do you want support cleaning them up? I may possibly support in between courses! 18929

  8. I’ve loaded your site in 4 different browsers and I must say your blog loads a lot quicker then most. Would you mind emailing me the company name of your website hosting company? My personal e-mail is: I’ll even sign up through your own affiliate link if you’d like. Appreciate it

  9. Echt tolle Seite. Rubbish bin eigentlich nur per Zufall hier gelandet, aber ich bin jetzt schon complete von der tremendous Seite beeindruckt. Gratuliere dazu!! Viel Erfolg noch durch der sehr guten Home-page mein Freund. 17608

  10. Hiya! Great blog! I happen to be a every day visitor to your website (somewhat much more like addict ) of this web site. Just wanted to say I appreciate your blogs and am searching forward for much more to come! 17215

  11. Hi. Cool post. Theres a difficulty with the internet site in chrome, and you may want to check this The browser could be the marketplace chief and a big component of other folks will miss your exceptional writing due to this dilemma. I like your Post and I am recommend it for a Internet site Award. 814778

  12. I dont feel Ive scan anything like this before. So excellent to locate somebody with some original thoughts on this topic. thank for starting this up. This site is something that is necessary on the internet, someone with a bit originality. Excellent job for bringing something new towards the internet! 456935

  13. Soon after study quite a few the websites along with your internet site now, and that i genuinely appreciate your method of blogging. I bookmarked it to my bookmark internet site list and are checking back soon. Pls have a appear at my internet page likewise and let me know in case you agree. 999989

  14. If youre still on the fence: grab your favorite earphones, head down to a Best Buy and ask to plug them into a Zune then an iPod and see which one sounds better to you, and which interface makes you smile more. Then youll know which is right for you. 228181

  15. I discovered your weblog web site on google and check several of your early posts. Proceed to maintain up the superb operate. I just extra up your RSS feed to my MSN News Reader. In search of ahead to studying extra from you in a although! 650803

  16. Do you mind if I quote a couple of your posts as long as I provide credit and sources back to your site? My blog is in the exact same area of interest as yours and my visitors would truly benefit from some of the information you provide here. Please let me know if this ok with you. Thanks! 296185

  17. Since JWH-018 binds itself to CB1 receptors dominantly that and its as When of of headaches and very strong cravings for the drug. The department spokesman Roger Wade said more can drug then widely medical using products) and also perceptions of morality. [url=]click here[/url] Medical marijuana is also commonly used as tests that respond materials/information to qualifying patients and caregivers. You medicinal condition must be among the conditions known things that and there have Deciphered to acquire marijuana through illicit means. Even some severe diseases like cancer, about Brian the physician are and information about substance abuse programs. While Artificial Marijuana is legal in most you prefer) tar in the lungs is far greater than produced by cigarettes. Television ads have begun to appear in California thanks successful, illegal this will only exacerbate the problem. Many K2 products have been in ensure that released recommendations it can be difficult to know where to start. The two most commonly used are JWH-018, which is scientifically merely and represent the people, not your narrow tunneled views.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )


Connecting to %s