Web form security - avoiding common mistakes

Today I thought I'd share a simple but very important lesson about the way forms interact with your server-side code, to help you understand and avoid some common security mistakes.

To try out the things I'm going to show you you'll need Firebug or Chrome Dev Tools or similar. Let's make two very simple pages so we have something to play with:


//index.php (this could just be html if you want)

<form action="data.php" method="post">
	Name:<input type="text" id="name" name="name">
	Age:<input type="text" id="age" name="age">
	<input type="submit" value="submit">
</form>

//data.php

$post = $_POST;

var_dump($post);

I'm going to put my files at http://localhost/tut/ - you can put it anywhere on your system, but I'd like to have a full address I can reference for explaining these concepts.

Since this post is more about thinking and understanding what is happening rather than code, a few pop quizzes as we go along to make you stop and consider.

Pop quiz #1: What is the difference between accessing http://localhost/tut/data.php, and accessing an API?

If you answered "nothing", go to the head of the class. An "Application Programming Interface" has a lot of rules, best practices, and formal definitions, but in layman's terms - it is just a way to talk to your website. So in oversimplified terms, every single url to your website is an access point of an "API".

Now, that's not the best definition to go through your programming life with, but to get you to understand the next concept, it will suffice.

Pop quiz #2: What did you make in index.php?

If you said, "A form", you get a Gentleman's C. If you said, "A convenient, graphical representation of how I want users to interface with my API", you're on your way to an A+.

You've all heard you "can't trust your user's data" and you always need to validate server-side, but what do we mean exactly? Well, what we mean is:

Your form is not part of your website.

Not in the way you think. When you first started creating sites, you put up a form for registration and you thought you could use it to control what the user sends you. You can't. Why not?

That form is not on your website. That form is on the user's computer.

That's right - that form is rendered in their browser. On their computer. If they "File | Save", they'll have that form as a file on their own computer, and it will still interact with your website. Even if they change it all up. Since you can't control their computer, you can't control their (what you thought was "your") registration form.

Why is this a danger to us? Let's do the following exercise:

Go to your index.php, use your Firebug (or whatever dev tool), and edit the html to add an additional field called "email".

Now, fill out your form - including the new field - and submit. You should see something like


array (size=3)
  'name' => string 'jeff' (length=4)
  'age' => string '12' (length=2)
  'email' => string 'jeff@codebyjeff.com' (length=19)
	

This is the most important thing to see and understand - ANYTHING I send to your data.php will show up in your $_POST, and you have no control over it. I don't even need a form - I just need your url.


// save as file curl.php

$url = 'http://localhost/tut/data.php';

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch,CURLOPT_POST, 1);
curl_setopt($ch,CURLOPT_POSTFIELDS,  "name=Joe&age=22&email=joe@codebyjeff.com");
$response = curl_exec($ch);
curl_close($ch); 

print_r($response);

So now that we understand that we can't control what is being sent to us, what should we look out for? In general, we want to focus on how we are updating & inserting data - particularly user data, but really any private data that should be restricted. Here are some examples:

Example 1 - getting primary keys from the $_POST

If you've read this far, this problem should jump right out at you. It is common these days, especially when working with frameworks, to use a database library for CRUD statements. So a not uncommon user update function might be something like:


$post = $_POST;

$this->db->update('users', $post['user_id'], $post);

where the update statement is created by asking for the table name (users), the primary key ($post['user_id']) and the data to update ($post), all of this passed in from the form.

Pop quiz #3: How would you exploit that security hole?

The answer is simply, pass a different user_id, username & password - like so:


//remember - a cracker is just making his own post however he likes

<form action="data.php" method="post">
	<input type="text" id="user_id" name="user_id" value="1">
	<input type="text" id="username" name="username" value="admin">
	<input type="text" id="password" name="password" value="cracked">
	
	<input type="submit" value="submit">
</form>

You see the cracker is trying to reset the main admin username and password, so they can log in with admin rights already enabled. How does he know the admin user_id is "1"? He doesn't - so maybe he'll just use the curl code above in a script that resets EVERYONE in about 30 seconds, or maybe he'll assume that the admin is the first user created and probably has a low user_id number.

Pop quiz #4: How would you exploit the following security hole?


CREATE TABLE `users` (
  `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(100) NOT NULL,
  `password` varchar(80) NOT NULL,
  `user_level` int(2) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8;

Again, the cracker passes in a user_level of "1" and sees if that is the admin level, then tries some others if not. This is a bit harder since he doesn't know the field name for permission levels - unless you've made another fundamental error. How many times have you seen this:


Error Number: 1054

Unknown column 'hackme' in 'field list'

INSERT INTO `users` (`hackme`, `password`,`user_level`) VALUES ('newguy', 'test', 1);

The error message will vary, and can be created in a variety of ways, but what they all have in common is: you have left sql debugging on and exposed your insert statement. The cracker simply creates a bad insert statement via your registration form to read the names of all your fields, and then uses his update crack from before to set his level to admin and take over your site.

So what are some of the basics we need to look out for when exposing update & insert statements to our users? Here are a few rules:

  • -- First, do not accept primary keys from the $_POST itself - get them internally via sessions or similar.

  • -- Always verify that the user has permission to update the record they are working on

  • -- Turn off all system debugging on production sites

  • -- As an extra precaution, permission levels should be a separate table joined to the users, not directly on the users table.

Look at any code you write that operates on your data through the above prism, and you'll sleep much better at night.

Hope that helps.

Contact me