5

First of all, I am fully aware of SQL injection vulnerabilities and I am using PDO for newer applications that I am developing in PHP.

Long story short, the organization that I'm working for cannot afford to delegate any human resources at the moment to switch everything over to PDO for the rather large application that I'm currently working on, so I'm stuck with using mysql_* functions in the meantime.

Anyways, I am wondering if it is safe to use data validation functions to "sanitize" numeric parameters used in the interpolated queries. We do use mysql_real_escape_string() for strings (and yes I am aware of the limitations there too). Here is an example:

public function foo($id) {
    $sql = "SELECT * FROM items WHERE item_id = $id";
    $this->query($sql); // call mysql_query and does things with result
}

$id id a user-supplied value via HTTP GET so obviously this code is vulnerable. Would be OK if I did this?

public function foo($id) {

    if (!ctype_digit($id)) {
        throw new \InvalidArgumentException("ID must be numeric");
    }

    $sql = "SELECT * FROM items WHERE item_id = $id";
    $this->query($sql); // call mysql_query and does things with result
}

As I'm aware, ctype_digit is the same as checking against a regular expression of \d+.

(There's also filter_var($id, FILTER_VALIDATE_INT), but that can potentially return int(0) which evaluates to FALSE under loosely-typed comparisons, so I'd have to do === FALSE there.)

Are there any problems with this temporary solution?

Update:

  • Variables do not only include primary keys, but any field with type boolean, tinyint, int, bigint, etc., which means that zero is a perfectly acceptable value to be searching for.
  • We are using PHP 5.3.2
rink.attendant.6
  • 44,500
  • 61
  • 101
  • 156
  • 5
    Consider using `is_int` as well. – Dai Jun 17 '14 at 07:11
  • 1
    @Dai The parameters are always coming through HTTP GET or HTTP POST so I believe they are guaranteed to be strings. – rink.attendant.6 Jun 17 '14 at 07:13
  • $id = (int) $id; before if (!ctype_digit($id)){ – Samar Haider Jun 17 '14 at 07:13
  • 1
    @SamarHaider That will always return `false`. – rink.attendant.6 Jun 17 '14 at 07:14
  • @rink.attendant.6 not in all cases no, he might manipulate the incoming data before sending it to the database. `is_int()` is just an extra precaution. – Zander Rootman Jun 17 '14 at 07:14
  • 1
    @SamarHaider If he convert `$id` to `int` `ctype_digit($id)` will always return `true` and It makes `$id` => `0` – Mohebifar Jun 17 '14 at 07:15
  • I'd just use `(int)` and then use `if ($id)` - on the assumption that you don't have a zero primary key. Casting any non-numeric data to int will just give you zero. – halfer Jun 17 '14 at 07:17
  • @halfer This doesn't only apply to primary keys, but to any field with integer types (e.g. `boolean`, `int`, `tinyint`, `bigint`) – rink.attendant.6 Jun 17 '14 at 07:17
  • Also, is it possible for you to _add_ a PDO option for new code? That should not cost you much in dev time, and will ease the switch when you come to make it. – halfer Jun 17 '14 at 07:18
  • 1
    `is_int` doesn't work on numeric strings (such as `'11'`) since it just checks if a variable has `int` type – hindmost Jun 17 '14 at 07:19
  • 1
    @rink (updated, now I've seen your question update) - OK, if zeroes are legitimate, then filtering is the way to go. Unit test it, and you'll be fine. – halfer Jun 17 '14 at 07:19
  • FYI, you can also use `(int) $id` – asprin Jun 17 '14 at 09:11
  • @asprin That might cast things to 0 which isn't exactly desirable in some situations. – rink.attendant.6 Jun 17 '14 at 09:12
  • 1
    Note that by default, PDO for MySQL emulates parametrised queries and so is still vulnerable to the same edge case sql injection issue as mysql_real_escape_string. – Kickstart Jun 17 '14 at 10:08
  • @Kickstart there are NO edge case sql injection issues, neither with mysql_real_escape_string, nor with PDO. Just set your encoding properly. – Your Common Sense Jun 17 '14 at 11:39
  • 1
    That is the edge case. If you make a mess of that (in a specific enough way that would almost need to be deliberate) then there is a possible way to suffer from sql injection, and pdo by default offers no more protection than mysql_real_escape_string. – Kickstart Jun 17 '14 at 11:52
  • @Kickstart yes, and mysql_real_escape_string offers perfect first-class protection - so, nothing to worry about – Your Common Sense Jun 18 '14 at 05:19
  • 1
    @YourCommonSense - point was the OP was worried about the limitations of mysql_real_escape_string as far as sanitizing strings. The main limitation if using it is the minor edge case, which applies equally to pdo. Either way it is nothing to worry about, or something to worry about in both cases. – Kickstart Jun 18 '14 at 08:20
  • @Kickstart Read my lips: **there is no such case and no limitations,** neither in mysql_real_escape_string not in PDO. It exists only in your imagination. – Your Common Sense Jun 18 '14 at 08:24
  • @YourCommonSense - Rubbish. It is an edge case and irrelevant almost all the time but it does exist and applies to both mysql_real_escape_string and default pdo use. Check the reply by ircmaxell here http://stackoverflow.com/questions/5741187/sql-injection-that-gets-around-mysql-real-escape-string – Kickstart Jun 18 '14 at 09:16
  • @Kickstart care to read **whole answer**, not only bold titles. Although this post is rather an empty rant and scaring tale than good research, it can be boled down to "configure your app properly and there will be **NOT a single problem**, without any edge cases". – Your Common Sense Jun 18 '14 at 09:27
  • @YourCommonSense - of course I read it. The whole point of that post is that if things are not configured properly (to the extent of pretty much needing to set things up to allow the problem) then there is an edge case that does give an issue. My point is this tiny issue applies also to PDO. If you are not going to listen then no point in continuing this. My comments quite clearly show what I mean and the link clearly shows the very limited situation that this can cause an issue. – Kickstart Jun 18 '14 at 09:42
  • @Kickstart when your application **is not configured properly**, it is not a "limited" situation. It is stupid situation. One can dig a ton of such "vulnerabilities" in a minute. But that's not vulnerabilities at all. Say, if you left your mysql root password blank and allowed outside access, **it is not a bug in mysql!** It is bug in your improperly configured application. Talk of edge cases only when properly configured software is still vulnerable. PDO is not. Have your version up-to-date, set proper charset in DSN - and noone will be able to inject anything. – Your Common Sense Jun 18 '14 at 10:34
  • 1
    @YourCommonSense - if you had bothered to read my comments you would see I have nowhere said that it is a bug in MySQL. It is a situation where sql injection can occur. Using pdo by default offers no more protection from this than old mysql_real_escape_string. The point is that the OP was concerned about the limitations of mysql_real_escape_string, and the main one is this theoretical edge case. This (non) issue applies equally to pdo that they mentioned they intended to switch to. – Kickstart Jun 18 '14 at 10:42
  • @Kickstart **THERE ARE NO LIMITATIONS IN mysql_real_escape_string**. Okay, as it seems you unable to comprehend the *analogy*, there is indeed no point to argue. Just remember the emphasized text above. – Your Common Sense Jun 18 '14 at 10:47
  • 1
    @YourCommonSense - It was the OPs choice of words to call it a limitation, after referring switching to pdo. However much you indulge in petty insults it does not alter the fact that under certain circumstances it is possible to suffer SQL injection via a parameter that has gone through mysql_real_escape_string, and that the **same applies to pdo**. Any such **perceived limitation** applies to both and in itself does not provide a reason to switch to pdo. That was the point of my original comment that still stands that you have been busy arguing against. – Kickstart Jun 18 '14 at 11:11
  • @Kickstart "under certain circumstances" an injection is possible in **everything**. So, there was no point in busying yourself with such a banality. – Your Common Sense Jun 18 '14 at 11:34
  • @YourCommonSense - The issue is the **perceived limitation** that the OP seemed to be worried about with pdo seemingly regarded as the solution. If they want to change to pdo for parameterised queries then great. My comment that you have chosen to argue against was highlighting that this **perceived limitation** is not relevant to making such a decision over their current solutions. – Kickstart Jun 18 '14 at 11:48
  • @Kickstart there is no "perceived limitation" but only a **delusion.** which is indeed shared for both mysql_real_escape_string and PDO. – Your Common Sense Jun 18 '14 at 12:52

6 Answers6

3

Yes, if you indeed religiously use the correct function to validate the data and correctly prevent the query from running if the data is not as expected, there's no vulnerability I can see. ctype_digit has a very limited and clear purpose:

Returns TRUE if every character in the string text is a decimal digit, FALSE otherwise.

There's basically nothing that can go wrong with this function, so it's safe to use. It will even return false on an empty string (since PHP 5.1). Note that is_numeric would not be so trustworthy. I would possibly still add a range check to make sure the number is within an expected range, I'm not sure what could happen with overflowing integers. If you additionally cast to (int) after this check, there's no chance of injection.

Caveat emptor: as with all non-native parameterised queries, there's still a chance of injection if you're getting into any shenanigans with connection charsets. The range of bytes that may slip through are severely limited by ctype_digit, but you never know what one could come up with.

deceze
  • 510,633
  • 85
  • 743
  • 889
1

Use mysql_real_escape_string and wrap your $id in single quote marks. The single quote marks ensures the safety and avoids the probability of SQL-injection.

For example, SELECT * FROM table WHERE id = 'escaped string' can't be hacked to something like: SELECT * FROM table WHERE id = 1; DROP table; as '1; DROP table;' will be considered the input argument for WHERE.

halfer
  • 19,824
  • 17
  • 99
  • 186
hansmei
  • 660
  • 7
  • 17
  • 2
    Why the single quote marks? If it is an integer, the database engine will have to do internal casting unnecessarily. – halfer Jun 17 '14 at 07:15
  • This solution doesn't work under [Strict SQL mode](https://dev.mysql.com/doc/refman/5.5/en/sql-mode.html). – rink.attendant.6 Jun 17 '14 at 07:26
  • @halfer yes, it will. Not a big deal. Don't be *that* scared by trifle things. – Your Common Sense Jun 17 '14 at 08:44
  • @rink.attendant.6 really? surely you have a proof? – Your Common Sense Jun 17 '14 at 09:17
  • The single quote marks ensures the safety and avoids the probability of SQL-injection. Eg: `SELECT * FROM table WHERE id = 'escaped string'` can't be hacked to something like: `SELECT * FROM table WHERE id = 1; DROP table;` as '1; DROP table;' will be considered the input argument for WHERE. – hansmei Jun 18 '14 at 10:16
  • @hansmei, thanks - good point. Admittedly the mysql library doesn't do multiple query execution, but it is wise to be safe. The extra detail you provided is normally best merged into your answer, so I've done that just now. – halfer Jun 20 '14 at 20:57
  • 1
    @halfer Thanks, I'll be sure to use tags to alert other commenters from now on:) – hansmei Jun 22 '14 at 11:16
1

Yes, it will work. Your code will raise an exception if the value isn't a numeric string, you'll just have to catch that and display an error message to the user.

Beware that ctype_digit($foo):

  • Will return true if $foo is empty before PHP 5.1 (see the doc) ;
  • Will return false for all int values of $foo outside of the [48, 57] interval (ASCII numbers).

So you'll also need to check that $foo is a non-empty string if you plan on using ctype_digit($foo)

svvac
  • 5,814
  • 3
  • 17
  • 22
  • We are using PHP 5.3.2 so the empty bug (feature?) isn't a problem. What did you mean by "return `false` for all `int` values of `$foo` outside…"? AFAIK, there are no decimal values/floats used anywhere in the application, not on the server-side anyways. – rink.attendant.6 Jun 17 '14 at 07:21
  • I mean that `ctype_digits(12)` returns `false`, as does `ctype_digits(32)`, but not `ctype_digits(49)` nor `ctype_digits('12')`. For the function to return `true`, the argument has to be a string composed only of numeric ASCII chars, or an integer between 48 and 57. If your value is *already* an integer, you won't get the result you're looking for. – svvac Jun 17 '14 at 07:25
1

Long story short, the organization that I'm working for cannot afford to delegate any human resources at the moment to switch everything over to PDO

I don't see where is the problem here. According to the code you posted, you are already using some sort of DB wrapper, and already planning to alter the calling code for the every numeric parameter. Why not to alter that DB wrapper to make it support prepared statements, and alter calling code to employ it?

Old mysql ext is not a problem - one can emulate prepared statements with it all right.

I am fully aware of SQL injection vulnerabilities.

Your "full awareness" is a bit exaggerated. Unfortunately, most people do not understand the real source of injection, as well as the real purpose of the prepared statement.

That thing with separating data from the query is a nice trick, but totally unnecessary one. While the real value of prepared statement is its inevitability, as opposed to essential arbitrariness of the manual formatting.

Another your fault is separated treatment for the strings - it is partly formatted in the query (adding quotes) and partly - outside (escaping special characters) which again is a call disaster.

As you decided to stick to the manual formatting, then enjoy your injections, sooner or later. Your ideas are good for the artificial, fully controlled sandbox example. However, things turn different in the real life application, with many people working on it. Instead of asking a program to format your data, you are asking people to do that. With all the obvious consequences.

It makes me wonder why PHP users unable learn from their mistakes, and still eagerly devising practices, that has been proved unreliable long time ago.

Just spotted another fallacy in your reasoning

a user-supplied value via HTTP GET so obviously this code is vulnerable.

You have to understand that any unformatted value makes this code vulnerable, no matter if its HTTP GET, FTP PUT or file read. It is not only notorious "user input" have to be properly formatted but any input. This is why it is essential to make DB driver the only place where formatting occurs. It should be not a developer who formats the data but a program. Your idea contradicts with such a cornerstone principle.

Your Common Sense
  • 156,878
  • 40
  • 214
  • 345
0

ctype_digit() will return false for most integer values of $id. If you want to use the function, cast it to string first:

public function foo($id) {

    $id = (string)$id;

    if (!ctype_digit($id)) {
        throw new \InvalidArgumentException("ID must be numeric");
    }

    $sql = "SELECT * FROM items WHERE item_id = $id";
    $this->query($sql); // call mysql_query and does things with result
}

This is because integer is interpreted as ASCII value.

Marek
  • 7,337
  • 1
  • 22
  • 33
-1

I use intval() for simple cases although (int) apparently eats less resources. Example:

$sql =
    "SELECT * FROM categories WHERE category_id = " .
    intval($_POST['id']) .
    " LIMIT 1";
halfer
  • 19,824
  • 17
  • 99
  • 186
tomhre
  • 295
  • 1
  • 4
  • 15
  • I can see why the original version acquired -2 in downvotes, but the new version is better. Please avoid asking questions in answers, even if you don't have rep to comment. – halfer Jun 17 '14 at 22:47
  • Yes, you can merge guest reputation into your account - I've done this. Log in as your current user, find the guest post and flag it, explaining that you'd like to have it merged in. Usually the guest post will be attached to an email address, so it may help if you can provide that as proof. – halfer Jun 17 '14 at 22:48
  • (To see how to get out of a ban, do a search on Meta Stack Overflow - plenty of info there). – halfer Jun 17 '14 at 22:49
  • the new version is NOT better. It's the same rubbish based on the ridiculous "eats less resources" delusion. – Your Common Sense Jun 18 '14 at 04:56
  • 1
    hey just trying to get out of ban here Common Sense so I have removed question from my "attempt" to answer, and thanks to Halfer for the advice – tomhre Jun 18 '14 at 10:45