Security - The PHP Project Guide (2014)

The PHP Project Guide (2014)

14. Security

Security is often looked past when writing any code, particularly when communicating with a database like MySQL. The reason for this is that it’s extremely easy to ignore escaping data ready to be used in queries, blindly outputting data from a database table and quickly setting sessions or cookies without researching the security considerations around these. We’ll start by looking at the biggest and most exploited, SQL injection. We’ll then move on to look at some less common problems, but equally as important.

14.1 Why no website is secure

When you’re building anything, you should be aware that it will never be 100% secure. This is due to the fact security vulnerabilities can be exploited several different ways and new vulnerabilities are always being discovered. If you’ve secure against something one way, there is a likely chance that it will be able to be exploited via another method. There is a concept called defence in depth, which helps but will still not render your code 100% secure. Let’s say you’ve secured your website as much as you possibly can from SQL injection and have taken precaution to connect to a MySQL server using secure login credentials. This is absolutely fine, however what happens when a security vulnerability becomes known for direct access to the server? When someone connects to your server and gains access to your data, how could you have known this was going to happen? The answer is you simply couldn’t, however defence in depth would mean that you would have encrypted any sensitive information and therefore would be useless to an attacker, e.g. passwords and credit card information.

Don’t worry though, it’s not something to completely panic about, despite the fact it sounds depressing. If you take care to secure your website as much as you possibly can, it will be difficult for the majority of attackers to breach your security.

14.2 Security first

This is so important to mention, because many people consider security as an afterthought. It’s easy to do this, so if you’re the kind of person that thinks like this, don’t worry. It’s natural to develop something and promise to yourself that you’ll fix it up later. As a developer you absolutely need to get into the habit of thinking about security as you develop. This isn’t just a case of using mysqli_real_escape_string in the odd place (a MySQLi function that strips data of potentially harmful characters that may assist SQL injection), it’s about looking at your website from every angle. What if someone changes this GET value? Can this form be spammed? How can I provide an additional layer of protected when a user is performing a critical action? Unfortunately for most people, the important of security is something that comes after an attack has taken place or they’ve written some code that’s been exploited. It’s difficult, because without being an expert in PHP security it’s sometimes hard to be fully aware of exactly how secure your website is. There are a host of resources and books that cover security, but remember it should be tailored for your code and not blindly applied.

14.3 What else?

You may have heard of SQL Injection and XSS attacks, but what else do we consider security? A popular attack that often brings websites down is something called a DOS (Denial of Service) or a DDOS (Distributed Denial of Service) attack. This consists of large amounts of traffic being sent to a server until it eventually can’t cope. Remember that servers have limited hardware and software capacity and in essence operate like a home computer, so can suffer from these kinds of attacks.

Malware is also a concern. Are you letting people upload files to your server? Are you protecting against the kind of file that can be uploaded properly? If not, it’s possible for worms, viruses, etc. to be uploaded or hostile code to be executed on your server. A single PHP file could wipe your entire public directory, leaving you with nothing.

14.4 Back up

If you’re scared already, I apologise! One way you can ease the pain of potential data loss is to back up, and back up regularly. This obviously isn’t an excuse to not implement good security. Backing up will mean that things can be restored in the event of a security attack and allows you to focus on fixing what went wrong and where. Remember that a database compromise can potentially reveal sensitive user data, so backup is simply a data loss precaution, not just something to fall back on. There are several ways you can back up your file and database data, and this can be automated and data can even be sent to a remote location. If you’re running something like Parallels or cPanel there are automated backup processes that allow you to send data to a remote FTP location. Useful!

Backing up database data is slightly more difficult as you’ll almost always need to write some code to do this for you, unless you can find an application that will connect and store this data for you.

It’s probably recommended to back up once a day but of course this depends entirely on your website and what you need to back up, and how often data is written to your database or file system. It also depends on how often code is released to the live server as part of development, as you’ll want to back this up too.

14.5 SQL Injection

Simply put, SQL injection is when an attacker crafts user input in such a way that it causes an SQL string to perform an unexpected and undesired query on a database. These can be deadly, with the ability to delete entire databases in some circumstances.

All data (whether specified by a user or not) that is used to form part of an SQL string, should be escaped. By escaped, we mean alter this data to ensure that it isn’t able to perform an SQL injection. There are many different ways to do this, but we’ll start with the most used example to explain how SQL inject could work, and then look at how we can protect against it. Consider the following query:

1 SELECT `user_id`, COUNT(`user_id`)

2 FROM `users`

3 WHERE `username` = '{$username}'

4 AND `password` = '{$password}'

This will return a count (1 or 0) as long as the username is unique (which we’ll assume it will be), telling us whether a specified username and password match, meaning a successful login. We’ll assume a plain text password for now, so we don’t get into encryption discussion. When the user provides a username and password, the query may then look like the following:

1 SELECT `user_id`, COUNT(`user_id`)

2 FROM `users`

3 WHERE `username` = 'ashley'

4 AND `password` = 'lawnmower'

Albeit being a weak password, this looks fine, and if the user ‘ashley’ did exist with the password ‘lawnmower’, this query would return 1. We may then go on to set a session containing the user’s ID as we’ve also specified this in the query. The danger here is that if this query was modified by SQL injection, it could return 1 regardless of whether this username and password combination had been found or not. Now, if we were to enter:

· The username as ’ OR ‘’= ‘

· The password as ‘ OR ‘’ = ‘’ AND user_id = ‘25

Then the SQL string would look like this:

1 SELECT `user_id`, COUNT(`user_id`)

2 FROM `users`

3 WHERE `username` = '' OR ''= ''

4 AND `password` = '' OR '' = '' AND `user_id` = '25'

Let’s evaluate this by hand. The query is checking that a username is equal to an empty string or an empty string is equal to an empty string (evaluates to true). It then requires that a password is equal to an empty string, or an empty string is equal to an empty string (which also evaluates to true). We then have an additional AND clause specifying the user_id must be equal to 1. This is something that will need to be guessed, but in reality isn’t that hard to. In effect we have two statements that have evaluated to true, and therefore the entire WHERE clause is true. We would now be logged in as the user with user_id 25.

This is a very basic example of SQL injection through user authentication. However, this isn’t limited to where a user specifically defines data. Watch out for values sent as query strings in URLs, posted as hidden form data or within cookie or session data. For example, you may be sending a variable via a URL query string to show a certain amount of posts per page. It’s likely that this would be placed directly into the query using the LIMIT clause and therefore can be manipulated, directly from the URL.

If you have hidden fields within a form then these need to be taken into consideration also, as it’s easy for an attacker to view and modify these. With cookie data, you may set cookies for particular settings. This could be a cookie to store a user’s particular setting, such as which page they were last looking at in an index of articles. Assuming you’d use this in a query with the LIMIT clause, this would need to be escaped also. These are a few examples of data that can be manipulated by users and some of the ways these values may be used in an attack, but pay attention to your own website and the data you’re passing to queries.

We’ll now look at how to escape data. We already know that single quotes can interfere with the flow of a query, so how do we escape these? The answer is to use the built in functionality from your database extension or think about the data you’re inserting and cast it to a particular type. There is another important consideration relating to PHP settings which we’ll also discuss - magic quotes.

However you’re connecting to your database, you’ll find a method to escape data. With the old mysql_ function extension, the function required is:

1 mysql_real_escape_string($string);

Previously there was mysql_escape_string, which should no longer be used. Take a look in the PHP manual to find out why. For MySQLi, which is recommended instead of the old MySQL extension, you could do this one of two ways, depending on your application style of development. This is the procedural and object oriented way.

1 mysqli_real_escape_string($string); // procedural

2 $db->real_escape_string($string); // oop, where db is the connection to your MySQ\

3 L database

For the majority of cases, this is enough to escape user input so you’re protected against SQL injection. The functions we’ve looked at above, however, can be used unnecessarily when we’re using certain data types. Let’s say you’re inserting an integer into a query. You wouldn’t need to use an escaping function because you know the data type and can cast this variable to a specific type:

1 $user_id = $_GET['user_id'];

2 $sql = "SELECT * FROM `users` WHERE `users`.`user_id` = {$user_id}";

The above simply concatenates the query and the supplied user ID from the global GET array. This user ID could be submitted as part of a form or supplied directly from a query string in a URL. Note, we’re not checking whether this user exists, or performing any other validation, simply to water down this example. Looking at our earlier examples, anything could be passed to this page and could form a dangerous query. If we know that the user_id field is of integer type, then we know it has to be supplied as an integer. So, to escape this data we can simply do:

1 $user_id = (int)$_GET['user_id'];

This simple addition will ensure that this variable is cast to an integer, removing any floating points or characters. For example, if the user ID was supplied as ‘1a’, our casting would leave this as 1. If it was supplied as ‘a1’, it would be 0. If we supplied it as 1.8, it would simply round down (note, this doesn’t round up) and leave 1. We’ve now cast a variable to a data type that we were expecting. Casting doesn’t have to just be for integer values. You can also cast to boolean, float, string, array, object, and can also use unset to cast to NULL. Casting in PHP is known as type juggling. As PHP is loosely typed, it doesn’t require casting when comparing or manipulating variables, such as concatenation or mathematical operations. Strictly typed languages like Java do require casting.

14.6 Magic Quotes

We’ll now look at magic quotes, what they are and what they do, and how to control this functionality based on your server settings. It’s important to establish that magic quotes has been deprecated since PHP 5.3 and actually removed in PHP 5.4 onwards. This was for very good reason, considering that magic quotes for many developers is something that has been ignored or not properly understood.

Basically, magic quotes is a feature of PHP that automatically escaped data passed to a PHP script. The plan here was to escape data globally so that anything passed to a query or anywhere that may pose a security risk was protected. Take the following line of code as an example:

1 $sentence = $_POST['sentence'];

If magic quotes were turned on, and we passed “It’s a really sunny day, let’s all go outside.”, we would end up with “It\’s a really sunny day, let\’s all go outside.”.

The danger with magic quotes is that it can be turned on and off within the php.ini file, the central file for a server’s PHP installation. This means that assuming that magic quotes has escaped all data on one server may mean that transferring to another server will leave the code open to SQL injection. Because some users may not be able to control their php.ini file through restrictions imposed by their hosting company, this would either mean that the entire code would have to be checked and data escaped, or a user would simply ignore it, which would avoid the point of magic quotes. Other reasons for magic quotes not being used is due to the fact that all data passed (e.g. from $_GET, $_POST, $_SESSION) is escaped, and this data may not need to be escaped, meaning that processing power is used unnecessarily. It makes sense to only escape the data that is not to be trusted. This also means that data is escaped and passed back in this form. For this reason, if a user were to type a comment into a comment box on your website and hit return to see a preview of their data, they would see the escaped content. For this reason, you’d have to use the PHP stripslashes function to remove the slashes before displaying this data to the user. This is a performance problem as you’re using another function without the need to. Overall, it made sense to remove this functionality from PHP, and rely on developers to escape the data by hand.

To check is magic quotes is turned on, you can simply look within your php.ini file for the following lines:

1 magic_quotes_gpc = Off

2 magic_quotes_runtime = Off

3 magic_quotes_sybase = Off

You can also turn off magic quotes by placing the following line within your .htaccess file if you don’t have access to your php.ini file, or can’t make changes to it.

1 php_flag magic_quotes_gpc Off

You can now develop as usual, and escape data when and where it needs to be. When you transfer your project to a live server, or switch server, you can rest in the knowledge that your website will be protected against SQL injection, regardless of the server’s PHP settings.

14.7 Session and cookie security

We’ll discuss session and cookie security together, and particularly in relation to user authentication. Typically, data identifying a user will be stored in a session, and could be initiated by a cookie if the user has chosen to be remembered (obviously, you’d never store data in a cookie like a user ID, as the value could then be easily manipulated within the browser). Using the most basic setup to store values in a session simply isn’t good enough, and we need to employ several other steps to ensure we make our website more secure. By all means, this isn’t going to be a detailed account into session and cookie security, but it should give you the ground to start to build on the security practices here and go and research further ways to secure your website.

When you store a value in a session or a cookie, this can be viewed by the user easily within a browser. At the time of writing, Firefox makes it particularly easy to view and modify session and cookie data. Try setting a cookie and a session on a page and open up the Firefox Firebug extension to take a look. You’ll find any sessions or cookies from the page you’re accessing under the cookies tab. Sessions are technically cookies, but are stored with a value that can be identified by the server, and then subsequently this value is looked up. If you’ve already examined with Firebug, you’ll see this hash. This means that the value can’t be read in plain text or manipulated meaningfully as the long string you see is only used to look up a server stored value. Cookies, however, store values in plain text and can be manipulated to potentially change the way code behaves. There are a also variety of ways that sessions can be manipulated and hijacked, so that an attacker can pose as someone else and do everything they’d be able to do if they’d logged in with their username and password.

Where are sessions stored?

Sessions are stored on the file system, where they hold the data you assign to them within your code. For example, if you were to do the following:

1 session_start();

2 $_SESSION['secret'] = 'tabby';

This creates a file with the following name within your PHP tmp directory. You can find out where this directory exists by either browsing around your PHP installation folder or taking a look in your phpinfo output.

sess_0613842845b85ca357ebe1d1eb0fb514

The last part of this filename is the session value within the browser. If you use tools like Chrome Developer Tools or Firebug to inspect these you’ll see this hash. This file contains the value:

secret|s:5:”tabby”;

If this file was somehow compromised, it could be used to set a valid session for another user and therefore take over an account.

So how can we make session storage more secure? A popular method is to store sessions within a database where access should be harder. PHP allows you to extend the functionality of its original session handler and from here you can store and retrieve sessions from a database. This is way beyond the scope of this book as it would involve listing a lot of code that would be non-specific to your website, however go ahead and look it up.

14.8 CSRF (Cross Site Request Forgery)

CSRF occurs when requests can be forged by providing a link which an unsuspecting user clicks on. This subsequently performs an action on your website that may have not been originally desired by the user. Let’s take an example:

1 <a href="/deleteaccount">Delete your account</a>

The following link may be presented to a logged in user to remove their account from your website. In this instance, the user may simply be presented with a dialogue, allowing them to confirm their decision to leave your website. This may look like the following:

1 <p>Are you sure you want to delete your account?</p>

2 <a href="/deleteaccount?confirm=1">Yes</a>

3 <a href="/">No</a>

Clicking ‘Yes’ will redirect the user to /deleteaccount?confirm=1 and will process their account deletion.

Now, if a user is still logged into your website and they’re browsing another website, they could be presented with the following link, either posted or hosted by someone:

1 <a href="http://www.phpacademy.org/deleteaccount?confirm=1">Click here for cute c\

2 ats!</a>

And your unsuspecting user has now had their account deleted on your website because you’ve not protected against CSRF. So, how do we protect against something like this? It’s actually rather simple and can be implemented quickly along with the rest of your link/form:

The first thing to do is set a session value, with a random hard to guess string:

1 // combine the result of the microtime function and the user's ID and md5 hash it.

2 $_SESSION['csrf_token'] = md5(microtime(true) + $user['user_id']);

The fact we’re using an md5 hash here doesn’t matter. What’s important is that this value can’t be easily replicated. Using a simple string of numbers like ‘12345’ as a CSRF token would be silly.

Now, output this token wherever you require protection:

1 <p>Are you sure you want to delete your account?</p>

2 <a href="/deleteaccount?confirm=1&token=<?php echo $_SESSION['csrf_token']; ?>">Y\

3 es</a>

4 <a href="/">No</a>

Or a form:

1 <form action="deleteaccount.php" method="get">

2 <p>Are you sure you want to delete your account?</p>

3 <input type="submit" value="Yes, delete my account">

4 <input type="hidden" name="confirm" value="1">

5 <input type="hidden" name="" value="<?php echo $_SESSION['csrf_token']; ?>">

6 </form>

All that’s left to do is match up the session value with the value retrieved from the result of clicking the link or submitting the form. In your confirmation page you may have the following code:

1 if ($_GET['c'] === $_SESSION['csrf_token']) {

2 // process user account deletion

3 } else {

4 header('Location: /');

5 }

If the value doesn’t match, we do something with the request. In this case, redirect. In situations like these, try to avoid outputting something like “Ha, got you!” or similar, as it gives an attacker more clues as to how your code is written (or unsuspecting users more confusion).

14.9 Include/require security

When talking about includes, I refer to using include, require, include_once and require_once, which all do more or less the same thing, just with minor variations. Consider an example when determining to show a user panel or admin panel to a logged in user. You may have a check to determine the access level of the user. For example:

1 if ($user->is_admin()) {

2 require '/includes/admin.php';

3 } else {

4 require '/includes/user.php';

5 }

This is fairly straightforward and to an untrained eye would seem insecure. The is_admin method deals with the check and will only include admin.php if true has been returned. We’ll assume that is_admin is fully functioning and working as expected. Now, if an attacker were to enter http://localhost/includes/admin.php, they would have immediate access to any admin functionality as the functionality within the include is not protected. Now consider that the code above remained the same, but a change was made to the admin.php file to be included:

1 // contents of the admin.php include

2 if ($user->is_admin()) {

3 /* admin page stuff */

4 }

When an attacker loaded up the URL above, they would be presented with a blank page. It’s a good idea at this point not to include an else statement with a message like “Sorry, you’re not allowed here!” as this is giving an attacker clues as to how your code works.

An alternative method of securing includes is to place them outside a publicly accessible location. This also means we don’t have to reuse the is_admin method when including the file and within the include itself. Placing admin.php outside the public folder may result in code like so:

1 if ($user->is_admin()) {

2 require '/var/secure/admin.php';

3 // or, if you're on a windows server

4 // require 'C:/secure/admin.php';

5 } else {

6 require '/includes/user.php';

7 }

Bear in mind that the above example assumes there are no other security exploits in your application that may allow access to the admin.php file, such as the ability for an attacker to traverse through files.

14.10 File traversal

When creating dynamic websites that allow values to be passed either from a session, GET or POST variables and placed into a function that accessing files, you’re automatically decreasing the level of security in your website. Consider an example where a user can upload a text document and then view it immediately. You may have functionality similar to the following:

1 header('Content-type: text/plain');

2 $file = $_GET['file']; // assume a value of 'notes.txt'

3 $dir = 'uploads/';

4

5 if (file_exists($dir . $file)) {

6 echo file_get_contents($dir . $file);

7 // will include 'uploads/notes.txt'

8 }

As it stands, the above code is extremely insecure. We’re assuming the contents of the GET value is ‘notes.txt’, but what if it were ‘../.htpasswd’ for example? Yes, you guessed it, we’ve just traversed back a directory by including ‘uploads/../.htpasswd’ and output the contents of the .htpasswd file. You may also assume that if only uploading text files then you could change the code for added security:

1 header('Content-type: text/plain');

2 $file = $_GET['file']; // assume 'notes'

3 $dir = 'uploads/';

4 $ext = '.txt';

5

6 if (file_exists($dir . $file . $ext)) {

7 echo file_get_contents($dir . $file . $ext);

8 // will include 'uploads/notes.txt'

9 }

So now if we provide the value of ‘../.htpasswd’ into the URL, we’ll be including ‘uploads/../.htpasswd.txt’ which doesn’t exist and therefore will not be output. However, on some servers a null byte can be used to ignore anything after the point where the null byte is inserted. A nullbyte is basically a control character representation of the value 0, and is specified as %00 in a URL.

This means that when we provide the value ‘../.htpasswd%00’, the .txt part of the include is effectively ignored and an attacker has successfully gained access to the .htpasswd file contents again. If this all sounds daunting then don’t let it. It’s a simply case of sanitizing all user input and not trusting anything else other than what you expect. In this example we could change the code to:

1 $file = str_replace(chr(0), '', $_GET['file']);

This essentially strips out a null byte character. If this confuses you, run the following code:

1 var_dump(chr(0));

And you should see the following output:

string ‘�’ (length=1)

Inspect the source code of the page you’ve output the above snippet on, and you’ll find the following character hidden amongst the PHP output formatting which was previously the question mark character:

1 &#0;

And that’s a null byte as an HTML entity!

14.11 Important considerations

When you’re building any software, security should be built into your application from the start. However secure you think something you’ve written is, there’s a chance that vulnerabilities may still exist. If in doubt, initially you should always assume that something along the line of your security process will go wrong. Let’s look at an example in which we read in GET data and output a file based on this given value.

1 $file = trim($_GET['file']);

2 $file_allowed = array('about', 'contact');

3

4 if (in_array($file, $file_allowed)) {

5 $path = 'includes/' . $file . '.php';

6

7 if (realpath($path)) {

8 include $path;

9 }

10 }

Here, we’re taking several precautions.

1. Trimming the input using the trim function to strip whitespace from the left and right.

2. Checking our value is one that is expected by using the in_array function to only allow specified values.

3. Using the realpath function to see if a file actually exists before including it.

Without security precautions, the code would read as follows:

1 $file = $_GET['file'];

2 include 'includes/' . $file . '.php';

14.12 Error reporting

You might be wondering what error reporting has to do with security. Take the following example. You have an /admin directory (not a great idea anyway) that contains an index.php file. This file contains a form to enter a username and password, and if correct, forwards the user on to administrate the website. Let’s pretend we’ve recently changed our MySQL user password and haven’t updated our code to reflect this. If the connection fails, you’d expect to see something like:

“Access denied for user: ‘alex’ to database ‘php’”

Oops, you’ve now revealed the name of the username connecting to the server and the name of your database. This may not seem like such a big deal, but it’s simply more that an attacker can add to their inventory against your website.

If somewhere along the line a query fails, you may see an error output like:

“You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ‘SELECT admin_id FROM admin WHERE username = ‘ at line 1”

This is of course an example, but entire SQL queries can be displayed revealing more about your database structure, depending on where the error appears in the query.

So how do we control error reporting? There are a variety of error reporting levels that we can set in our PHP configuration, and we can also turn it off altogether. This is recommended for production environments. You’ll be interested in the following lines in your php.ini file:

1 display_errors = 1;

2 error_reporting = E_ALL;

In the above configuration, we’re enabling error reporting and using E_ALL to dictate what kind of errors should be displayed. Simply changing the 1 to a 0 on the display_errors property will turn it off. You don’t need to make any amends with the error_reporting property. You can also change the error reporting level, and you’ll find some great documentation and examples within your php.ini file that’ll tell you more about this.

14.13 error_log

The error log is by default stored within the directory of the file the error originated from and will be stored with the file name error_log (no extension). This is great for development, as you can pinpoint where something is going wrong, open up a log and inspect it for the errors that are preventing you from continuing. These logs however, are available for anyone to access unless you either disable their functionality or you specify that they be saved in a different location. For a development environment that can’t be accessed by the public, this is fine to leave, however within a production environment you can take action and change the default behaviour of this. Head over to your php.ini file and look for the following lines:

1 log_errors = On

2 error_log = /path/to/error.log

It’s preferable to store error logs outside of your public directory or if not, store within a folder where you can define no access using an .htaccess file. For example, an .htaccess file to deny directory to all files within a directory would look like:

1 Deny From All

Simple, now you’re logging errors to one location that can’t be accessed within the browser, but you can still view. Of course, you could always just turn error logging off!

14.14 Passwords

When you use hosting, you’ll obviously use passwords to access the services being hosted. This includes any control panel you’re using, your billing area, MySQL and more.

Passwords are often overlooked in security, and it’s really important that any passwords that even remotely relate to your server are long and strong. You may not think that your billing area needs a particularly strong password. What’s the most an attacker will do here? Pay a bill? Unlikely, but they could gain access to information relating to your account. What happens when they contact customer services to have your account removed?

14.15 You online

This brings us to my next point nicely, which is what you write and share about yourself publically online. Basically, the more information available about you online, the more likely it is that someone can impersonate you. This might involve someone breaking into one of your accounts by using secret information (e.g. “What was the name of your first pet?”) to reset your password, which could easily be obtained from a social networking site you’re on. Often overlooked, this is an important thing to watch out for.

1. Keep as much personal information offline as you can.

2. When entering answers to secret questions, use bogus information to deter people using this to reset your password.

14.16 What next?

Security is something that will always be an issue. Whatever the environment, you should always consider security and build it into your application. Security should never be an afterthought. The key is to never trust input, ensure you have validation present on all input even when coming from trusted sources and to sanitise before storing data.

We’ve not actually covered every security problem you may run into when developing, but we’ve covered the most common. Now you’ve seen the basics, you can at least apply these to your website and start to tailor more advanced and more extensive security into what you’re doing.

14.17 Recommended reading

I highly recommend visiting the PHP Security Consortium which is an ever expanding resource for security concerns around PHP. The founder has also written a book called Essential PHP Security which is a great book if you’re just starting to learn about PHP security or want to top up or clarify any knowledge you currently have.