0

I've gotten into the habit of writing the following sort of code:

$q = mysqli_query($mysqli,"SELECT * FROM table WHERE a='$a', b=$b;");
while ($row = mysqli_fetch_array($q)) {
    // do something
    }

Where $a is a string entered by the user (gotten through $_GET) and $b is a user-entered integer.

Obviously the code I have above is vulnerable to SQL injection attacks, so my habit is to rewrite it like this:

$q = mysqli_query($mysqli,"SELECT * FROM table WHERE a='".str_replace("'","",$a)."', b=".($b+0).";");

But this of course has problems if $a needs to have apostrophes (or quotation marks when quotation marks are used to mark the string).

Recently I learned about prepared statements in mysqli and started playing around with them. I wrote the following function to make it easier to make calls without having to change much of my code:

function safequery($a,$b,$c) {
    global $mysqli;
    $q = mysqli_prepare($mysqli,$a);
    $e = "mysqli_stmt_bind_param(\$q,\$b";
    $i = 0;
    while ($i < count($c)) {
        $e.=",";
        $e.="\$c[$i]";
        $i++;
        }
    $e.=");";
    eval($e);
    mysqli_stmt_execute($q);
    return $q;
    }

safequery("SELECT * FROM table WHERE a=? AND b=?;","si",array("unsafestring",37));

But what is returned from this function turns out not to be a mysqli_result and thus doesn't work with the first bit of code above. After some more research, I found an alternative, but it would require a complete rethink of how I write my code. Is this necessary or is it possible to protect against MySQL injection attacks with only small changes to the first bit of code (no new lines, same output style, etc.)?

I have looked around on StackOverflow and the rest of the web but I can't find a good simple solution; all of them require the edition of at least three more lines for every call and a different way of reading each row. I'd prefer to do this procedural-y...

Laef
  • 1,086
  • 2
  • 11
  • 27
  • 3
    My vote is to redo the whole thing. You should find these answers helpful http://stackoverflow.com/questions/5100046/how-to-bind-mysqli-bind-param-arguments-dynamically-in-php – Jeff Puckett Jun 11 '16 at 01:33
  • 1
    "I wrote the following function..." This looks like your third horrible "habit" in a row. Have you considered doing it *the right way* for a change? – ceejayoz Jun 11 '16 at 01:54
  • 3
    Your new "safer" way of doing it is actually the **most** disastrous of the three, by the way. An attacker could run **any arbitrary PHP code they felt like** in your `eval()` call. You're like a surgeon fixing a broken bone by *amputating the head*. – ceejayoz Jun 11 '16 at 01:56
  • 2
    @ceejayoz "The patient is having a heart-attack. I suggest placing explosives on their chest and jump-starting it that way!" – tadman Jun 11 '16 at 01:57
  • @ceejayoz What is the right way in this case? If I spend my time rewriting the same code over and over again, I'm more likely to make a mistake in it. A function to make the script more DRY is usually expected of a programmer. Also, I am interested into how `eval()` could cause trouble in this particular implementation. A user entered string does not get evaluated so I'm surprised that it would cause any problems; it just executes some code that calls on the values of user-entered variables. – Laef Jun 11 '16 at 02:57
  • 3
    You stated `$b is a user-entered integer` and you're not doing your `$b+0` hack to force integer casting (`$b = (int)$b` would be safer, but that misses the point). If you want to DRY, use one of the many battle-hardened and extensively tested ORMs. Code that runs `eval` is rarely if ever necessary, and any experienced PHP programmer knows it signifies **here be dragons**. – ceejayoz Jun 11 '16 at 03:00
  • 2
    Since you are "interested into how `eval()` could cause trouble in this particular implementation", enter `1) ; system(\"/rm -rf /\"` in your input field. And now the disclaimer: do not do that. – Solarflare Jun 11 '16 at 11:55

2 Answers2

1

Don't think half-measures are going to solve this problem. Commit to expunging all of the interpolation bugs from your code and be disciplined about using prepared statements. Your proposed fix only makes things worse, it gives you a false sense of security. It's also considerably more work than using prepared statements so I'm not sure why you'd even bother doing it this way.

One way to make this a lot easier to do is switch from using double quotes " to single quotes ' on your queries to disable interpolation. Any escaping errors become syntax problems, and if your editor highlights those you'll be able to spot them from across the room, and if something does by fluke work you'll be inserting harmless things like $a instead of actual data.

Another thing to consider is if you should be using an ORM like Doctrine or Propel given what you know about the sophistication of your application. These can make things considerably easier from an implementation perspective.

The code you have there is a ticking time bomb, get rid of it as soon as you can. Don't think replacing quotes is enough, that solves just one issue, there's actually a number of other methods your application can be vulnerable to injection bugs. Tools like SQLMap have an entire arsenal of things they can try to break your code and if you look at the list of things it can do if it finds a flaw you'll probably want to fix these problems right away.

One way you can find issues is using a tool like grep:

grep query `find -name '*.php'` | grep '\$'

That's not bulletproof, but it should probably turn up a lot of code you should fix right away.

Also as @ceejayoz suggests, purge that function with eval in it from your computer and never, ever do that again.

tadman
  • 208,517
  • 23
  • 234
  • 262
  • I'll look into the first three things you linked to. However, I am interested into how `eval()` could cause trouble in this particular implementation (I've spent a lot of my time warning people away from `eval()` in the past). A user entered string does not get evaluated so I'm surprised that it would cause any problems; it just executes some code that calls on the values of user-entered variables. But I'd love to know more about why you and @ceejayoz think it would be a problem. – Laef Jun 11 '16 at 02:54
  • @laef `eval` is *always* trouble because if you really need to use it, you need to be absolutely positive it can't be used to execute arbitrary commands. As you've already shown this code is receiving random user data, this is the worst possible case, taking a serious problem and making it apocalyptically bad. Just use placeholders. – tadman Jun 11 '16 at 02:55
  • I still don't agree with the assessment that `eval()` could cause trouble in this case but I do agree that placeholders would be a far better alternative. What would be the correct implementation here? – Laef Jun 11 '16 at 03:01
  • 2
    If you *must* do something this crazy, use [`call_user_func_array`](http://php.net/manual/en/function.call-user-func-array.php) instead of `eval`, you have way more control over what's going on and you're guaranteed your arguments are being passed through as arguments. The best way to fix this is to nuke it from orbit and replace it with something sane like `SELECT * FROM table WHERE a=?, b=?` and use `bind_param` to apply the data properly. – tadman Jun 11 '16 at 03:03
  • Yes, I spent some time looking over it some more and playing around with some other options and `call_user_func_array` certainly is the best way of doing this. Thanks for suggesting it. Random note: `$a` within the function is, as a matter of fact, `SELECT * FROM table WHERE a=?, b=?;`, or some other similar parameterised string. I should have made that clear. Sorry for being so dense. Looking back at it, you're right. Thanks a lot! – Laef Jun 11 '16 at 03:50
  • 1
    The more layers of confusion you have in your queries the higher the probability you'll have severe bugs. Parameterized queries are usually very direct, easy to read, and mistakes are obvious. You might have a better time with PDO if you're in there updating things, the named placeholders feature is a huge advantage and it's also not MySQL specific. – tadman Jun 11 '16 at 03:53
1

After talking with the commenters and looking at some of the other questions and things they linked to and suggested, I've rewritten the third code snippet to both solve the problem in question and fix the security holes that several commenters pointed out (if there are any remaining ones, please tell me).

First on my use of eval(). Though I can't see any immediate way it could cause problems (user strings are not executed as code in the eval()) it is certainly a round about and stupid way of solving my problem. @TadMan suggested call_user_func_array() which, once worked out, looks something like this:

function refValues($arr){
    if (strnatcmp(phpversion(),'5.3') >= 0) //Reference is required for PHP 5.3+
    {
        $refs = array();
        foreach($arr as $key => $value)
            $refs[$key] = &$arr[$key];
        return $refs;
    }
    return $arr;
}
function safequery($a,$b,$c) {
    global $mysqli;
    $q = mysqli_prepare($mysqli,$a);
    call_user_func_array("mysqli_stmt_bind_param",refValues(array_merge(array($q,$b),$c)));
    mysqli_stmt_execute($q);
    return $q;
}

safequery("SELECT * FROM table WHERE a=? AND b=?;","si",array("unsafestring",37));

It turns out that mysqli_stmt_bind_param() takes only references, thus the new refValues() function (run into and solved before on StackOverflow: https://stackoverflow.com/a/16120923/5931472).

While this removes eval() and makes my code easier to understand, it still doesn't solve the original problem of returning the query response in a way that mysqli_fetch_array() can use. It turns out that the proper function to do this is mysqli_stmt_get_result() so the last line of safequery() is rewritten from:

return $q;

To:

return mysqli_stmt_get_result($q);

The result of safequery() is then a mysqli_result which can be used by mysqli_fetch_array().

Community
  • 1
  • 1
Laef
  • 1,086
  • 2
  • 11
  • 27
  • 1
    As an illustration of why you should strongly consider an ORM, here's what your entire query would look like in Laravel's Fluent query builder (which can be used outside of Laravel just fine): `DB::table('table')->whereA('unsafestring')->whereB(37)->get()` – ceejayoz Jun 11 '16 at 18:22