Blog

How (not) to use request_var

Posted by igorw in Modifications with the tags , on September 10th, 2009

Note: This post is targeted at MOD authors and contains many technical details.

Introduction

Amongst the great security features that phpBB 3.0 provides is the function used for processing user input, request_var.  This function was designed to make it easy to securely retrieve user inputted data.  It is one of the most important security functions in a system that retrieves external data as it can (with caveats that will be elaborated upon) single-handedly stop XSS and SQL injection attacks dead in their tracks

The reason we have created this blog post is to give more information to modification developers on how to properly explain how this works and why you should use it.

SQL Injections

SQL injections are one of the most dangerous vulnerabilities a web application can have, yet are one of the easiest vulnerabilities to prevent.  These attacks happen when data is inserted into the database without properly sanitizing.  Anything from the $_GET, $_POST, $_COOKIE, or $_SERVER arrays should be considered user input and require sanitizing.

To prevent SQL injections, proper use of request_var’s data typing is required.

Data typing

In order to prevent SQL injections on non-strings it is important to set the data type on the input.  It is absolutely crucial to make sure the type is what you expect it to be.

In request_var this is done by casting the second argument, the default value, to the type you need.

Example usage

Here is an example of how to use request_var.  It obtains user input by using request_var and runs an SQL query based on it.  Note the 0 in the request_var call, it makes sure that $user_id is a number.

Code: Select all
$user_id = request_var('user_id', 0);

$sql = 'SELECT username
FROM ' . USERS_TABLE . '
WHERE user_id = ' . $user_id;
$result = $db->sql_query($sql);
$row = $db->sql_fetchrow($result);
$db->sql_freeresult($result);

echo $row['username'];

Where things can go wrong

Here is an example of a mistake which can have very bad consequences.  It is based on the previous example.  Instead of defaulting to 0 we want to default to the current user’s user_id.

Code: Select all
$user_id = request_var('user_id', $user->data['user_id']);

$sql = 'SELECT username
FROM ' . USERS_TABLE . '
WHERE user_id = ' . $user_id;
$result = $db->sql_query($sql);
$row = $db->sql_fetchrow($result);
$db->sql_freeresult($result);

echo $row['username'];

As you can see, only the first line has changed.

The vulnerability

Did you spot it? $user->data[‘user_id’] is a value that is taken directly from the database and all values from the database are strings. This means that $user_id will not be cast to an integer but instead to a string.  The consequence is that an attacker can alter the SQL query to one that retrieves or changes specific data in your database for their own use.

If, for example, an attacker wanted to get some email addresses, he could simply set the user_id GET parameter to:

Code: Select all
0 UNION SELECT user_email as username FROM phpbb_users WHERE user_id = 2

This would result in a final SQL query like this:

Code: Select all
SELECT username FROM phpbb_users WHERE user_id = 0 UNION SELECT user_email as username FROM phpbb_users WHERE user_id = 2

That would give him the email address of the user with user_id 2.  It should be pretty clear that there is a lot of bad stuff that can be done using this method.

How to do it properly

It is extremely easy to prevent this from happening, the only required change is casting the second argument of request_var to an integer.  In code this would look as follows.

Code: Select all
$user_id = request_var('user_id', (int) $user->data['user_id']);

One more note for strings

Although request_var can protect you for most types when properly used, there is still one more requirement if you are requesting a string.  That is to escape the string using the correct escaping method for your database, which is all handled correctly using the sql_escape function in the database object.

Code: Select all
$foo = request_var('foo', 'bar');
$sql = 'SELECT username
FROM ' . USERS_TABLE . '
WHERE foo = ' . $db->sql_escape($foo);

That’s it, folks!

request_var is a great tool but it needs to be used correctly, which includes casting the default value to the correct type and escaping any strings before using them in an SQL query.

Resources

It is one of the most important security functions in a system that retrieves external data as it can (with caveats that will be elaborated upon) single-handedly stop XSS and SQL injection attacks dead in their tracks.

20 Responses to “How (not) to use request_var”

Posted by Dog Cow on September 10th, 2009 at 10:34 pm:

Good advice.

To add: don’t quote numbers! Just don’t do it anywhere, unless you specifically want to output it as a literal.

Posted by onehundredandtwo on September 11th, 2009 at 9:47 pm:

or if you used is_numeric() for the user_id you would be safe.

Posted by onehundredandtwo on September 11th, 2009 at 9:51 pm:

^ sorry, I meant using an if statement using is_numeric, not validating it with it, if that’s what people thought I meant. 🙂

Posted by Deadpool on September 12th, 2009 at 4:42 am:

Why use is_numeric() when you can just type cast to an integer?

Posted by Igor Wiedler on September 12th, 2009 at 11:16 am:

The idea is really to use casting for this. That’s what data types exist for, after all.

Posted by Josh on September 25th, 2009 at 5:28 am:

Why not just assume all user input is unsanitary and escape everything as strings in the first place before sending it to db?

Posted by evil3 on September 25th, 2009 at 3:39 pm:

That’s precisely what request_var does.

Posted by Man on October 7th, 2009 at 9:18 am:

$user_id = request_var(‘user_id’, $user->data[‘user_id’]);
$user_id = ($user_id + 1) – 1;

Posted by evil3 on October 7th, 2009 at 12:50 pm:

That would be a possibility, but again. Why not just use:
$user_id = request_var(’user_id’, (int) $user->data[‘user_id’]);

Posted by Obsidian on October 8th, 2009 at 8:58 pm:

Josh, typecasting integer values is far more efficient than escaping everything as strings. 😉

Posted by drathbun on October 12th, 2009 at 4:23 pm:

How is a wanna-be hacker going to overwrite the value for $user->data[‘user_id’]? Unless your database has been compromised, shouldn’t you trust data from that source?

I may be missing something, but I can’t see how this situation could ever occur. 😕

Posted by Dan on October 16th, 2009 at 9:28 pm:

@drathbun, it was explained in the post. $user->data[‘user_id’] is a string “12345”, not a number. It’s only the default value, used if the input is not supplied. When the attacker sets the user_id cookie to ‘0 union BLA’, because you’re requesting a string-type input it gets passed through unchanged. If you had used a numeric 12345, it would cast user_id to a numeric and the value would be 0. (Or throw an error, or whatever else request_var does)

Posted by Igor Wiedler on October 19th, 2009 at 1:49 pm:

Exactly. The point is: not the content of $user->data[‘user_id’] matters, but the type. And in PHP everything coming from the DB is a string. Because request_var relies on the type of the default value, you need to make sure it also has this type. Otherwise the user input is not cast to that type (in this case int).

Posted by John Wells on December 4th, 2009 at 4:10 am:

could this not also be fixed in request_var itself? It could try to cast default parameters to an integer or float by default.

For example, it could cast the parameter to a string and then an integer, then back to a string. If the parameter remains the same after the double-cast, then cast the request_var result as an integer. Same for float.

It doesn’t obviate the need for proper user code, but it makes it just a little more secure, and I can’t imagine it would have much overhead.

Posted by evil3 on December 4th, 2009 at 9:48 am:

John Wells: I don’t quite understand how your double-cast would work. Let’s assume the user enters the string “evil3” as his username. If request_var were to cast it to an int, the result would be 0. Casting it back to a string would then change that to “0”.

It’s not that easy. Unless there’s something I’m missing, of course. 🙂

Posted by John Wells on December 8th, 2009 at 12:58 pm:

Evil<3:

I mean the default parameter, rather than the user code. The request_var function could not only sanitize the user input, but also the default parameter, as it is so often misused in MODs.

This would do the trick, as the first two lines of function request_var in functions.php:

Code: Select all

$default = ((float)$default == (str)$default) ? (float)$default : default;
$default = ((int)$default == (str)$default) ? (int)$default : $default;

It wouldn’t protect against stupid use of use input, but it would protect specifically against the above example, “Where things can go wrong”.

Posted by evil3 on December 8th, 2009 at 9:01 pm:

Ah, now I see what you mean. That would be a possibility, but might yield unexpected results. Your example implementation could lead to problems since in PHP == would match many things.

0 == ” == 0.0 == ‘0’

Posted by Stephan on January 20th, 2010 at 8:44 am:

I understand the idea of request_var and it has its uses.
I also understand the idea of $db->sql_escape().
But it seems like such a stretch of imagination to justify and explain request_var as a SQL injection prevention measure. sql_escape ensures that more than request_var. If you rely on request_var for SQL injection protection, you have bigger problems to worry about.

Want to protect yourself from SQL injection? There’s only One True Way (TM): Prepared statements and parameter binding.

In my opinion, the request_var (at least how it’s documented in your wiki) is only going to give you a false sense of security.

Web application security is hard work with lots of nitty-gritty implementation details that you must be aware of and There Is No Silver Bullet.

Posted by evil3 on January 20th, 2010 at 8:00 pm:

Staphan: I agree that prepared statements are the ideal way to do this. phpBB 3.0 does however not support prepared statements, so it will have to build your SQL manually.

sql_escape only handles string escaping. Most/many SQL queries rely on integers (WHERE foo_id = 2). For those request_var is great because it ensures the type is really an int by casting it. That’s where the hidden value of request_var lies.

Commenting is disabled for this blog post