2

I've watched Computerphile's video many times on this subject(for any of you who want, this is the link: https://www.youtube.com/watch?v=_jKylhJtPmI). He provides some really good advice on how to combat SQL Injection and make your app more effective. These are the key points from his video:

  1. Don't use straight and unprotected SQL commands because this is the way hackers can perform a SQL Injection, stealing, modifying, or even deleting your data.
  2. A good approach is to use the mysql_real_escape_string(String s) function. This basically places on the start of every dangerous character (/,", {, }, etc) a slash (/). So basically this makes the quote or slash inside the string useless.
  3. The best thing to do is to use prepared statements. So, you basically say:

    SELECT * FROM USERS WHERE username = ?
    

    Later you go and replace the question mark with the string you want to input as the user name. This has the advantage of not confusing PHP or any other fault-tolerant language, and using this simple and (kind of, hacky) elegant solution to just say replace this with the string and tell the language that what is given is just a string and nothing more than that.

That is good and all, but this video is really outdated. It was made way back in 2013 and since then a lot of new technology has emerged. So, I tried to search the internet to find if there were any new approaches or if this is the one. But the problem was that either I couldn't find it or either I found something that was super confusing.

So, my question is: Is there a better and enhanced way to combat SQL Injection that has been introduced, or if prepared statements are still the norm and if they are vulnerable to any kind of attack?

Gaurav Mall
  • 2,372
  • 1
  • 17
  • 33
  • 2
    The basic idea is to pass values as parameters into the SQL statement. `?` is one syntax. Named and positional parameters are others. But the basic idea is correct. – Gordon Linoff Jul 29 '19 at 14:50
  • So, you mean prepared statements should be used? – Gaurav Mall Jul 29 '19 at 14:50
  • 5
    Yes, prepared statements and parameter binding is still the best way to go. – aynber Jul 29 '19 at 14:51
  • Can you please post an answer explaining what parameter binding is? I'm a bit of a beginner in that aspect of SQL. Thanks – Gaurav Mall Jul 29 '19 at 14:52
  • 5
    ...and using `mysql_real_escape_string(String s)` is outdated. That's a no-go. – trincot Jul 29 '19 at 14:53
  • @trincot I know that I used it once and one of my colleagues deleted the whole database... – Gaurav Mall Jul 29 '19 at 14:53
  • 1
    Here are the instructions for parameter binding for [mysqli](http://php.net/manual/en/mysqli-stmt.bind-param.php) and pdo ([bindParam](http://php.net/manual/en/pdostatement.bindparam.php) or [bindValue](http://php.net/manual/en/pdostatement.bindvalue.php)). Basically using `?` or `:name` is parameter binding. I say both prepare and binding because some people will just do prepare and do it improperly. – aynber Jul 29 '19 at 14:54
  • Oh, thanks I wanted an explanation for PDO, and it works great! – Gaurav Mall Jul 29 '19 at 14:56
  • 1
    @trincot while it *is* outdated, the issue is that it has **flaws** as shown by ircmaxwell (on here and elsewhere) that the correct set of characters can circumvent / cancel out the escapism of `mysqli_real_escape_string` – Martin Jul 29 '19 at 16:41
  • @Martin, I suppose your comment is to the OP. I know that -- it's a no-go. – trincot Jul 29 '19 at 17:05
  • @trincot I simply wanted to give a detail as to one of the reasons *why* it's outdated. `:-)` – Martin Jul 29 '19 at 22:26
  • [Using parse tree validation to prevent SQL injection attacks](https://www.google.com/search?q=Using+parse+tree+validation+to+prevent+SQL+injection+attacks) also can be a very good approach, Problem is you would need to write a fullblown SQL parser for your application.. – Raymond Nijland Aug 02 '19 at 15:25
  • ...The general idea is to parse a SQL statement into a tree and compare that tree with a allowed tree structure.. When a simple `UNION` is added to inject something the structure is changed and the application should disallow the query to run so this would stop first and second order (a.k.a stored) SQL injections death in their tracks as MySQL never sees the query. – Raymond Nijland Aug 02 '19 at 15:25

1 Answers1

5

Parameter binding is still the best solution in most examples of combining dynamic data with an SQL query.

You should understand why. It's NOT just doing a string substitution for you. You could do that yourself.

It works because it separates the dynamic value from the SQL-parsing step. The RDBMS parses the SQL syntax during prepare():

$stmt = $pdo->prepare("SELECT * FROM USERS WHERE username = ?");

After this point, the RDBMS knows that the ? must only be a single scalar value. Not anything else. Not a list of values, not a column name, not an expression, not a subquery, not a UNION to a second SELECT query, etc.

Then you send the value to be bound to that placeholder in the execute step.

$stmt->execute( [ "taraiordanov" ] );

The value is sent to the RDBMS server, and it takes its place in the query but only as a value and then the query can be executed.

This is allows you to execute the query multiple times with different values plugged in. Even though the SQL parser only needed to parse the query once. It remembers how to plug a new value into the original prepared SQL query, so you can execute() as many times as you want:

$stmt->execute( [ "hpotter" ] );
$stmt->execute( [ "hgranger" ] );
$stmt->execute( [ "rweasley" ] );
...

Are prepared statements the best? Yes, they are. It doesn't matter that the advice comes from 2013, it's still true. Actually, this feature about SQL dates back a lot further than that.

So are query parameters the foolproof way of defending against SQL injection? Yes they are, if you need to combine a variable as a value in SQL. That is, you intend for the parameter to substitute in your query where you would otherwise use a quoted string literal, a quoted date literal, or a numeric literal.

But there are other things you might need to do with queries too. Sometimes you need to build an SQL query piece by piece based on conditions in your application. Like what if you want to do a search for username but sometimes also add a term to your search for last_login date? A parameter can't add a whole new term to the search.

This isn't allowed:

$OTHER_TERMS = "and last_login > '2019-04-01'";
$stmt = $pdo->prepare("SELECT * FROM USERS WHERE username = ? ?");
$stmt->execute( [ "taraiordanov", $OTHER_TERMS ] ); // DOES NOT WORK

What if you want to allow the user to request sorting a result, and you want to let the user choose which column to sort by, and whether to sort ascending or descending?

$stmt = $pdo->prepare("SELECT * FROM USERS WHERE username = ? ORDER BY ? ?");
$stmt->execute( [ "taraiordanov", "last_login", "DESC" ] ); // DOES NOT WORK

In these cases, you must put the column names and syntax for query terms into your SQL string before prepare(). You just have to be extra careful not to let untrusted input contaminate the dynamic parts you put in the query. That is, make sure it's based on string values you have complete control over in your code, not anything from outside the app, like user input or a file or the result of calling an API.


Re comments:

The idea Martin is adding is sometimes called whitelisting. I'll write out Martin's example in a more readable manner:

switch ($_GET['order']) {
case "desc": 
  $sqlOrder = "DESC"; 
  break; 
default: 
  $sqlOrder = "ASC"; 
  break;
}

I replaced Martin's case "asc" with default because then if the user input is anything else -- even something malicious -- the only thing that can happen is that any other input will default to SQL order ASC.

This means there are only two possible outcomes, ASC or DESC. Once your code has complete control over the possible values, and you know both values are safe, then you can interpolate the value into your SQL query.

In short: always keep in your mind an assumption that $_GET and $_POST may contain malicious content. It's easy for a client to put anything they want into the request. They are not limited by the values in your HTML form.

Write your code defensively with that assumption in mind.


Another tip: Many people think that client input in $_GET and $_POST are the only inputs you need to protect against. This is not true! Any source of input can contain problematic content. Reading a file and using that in your SQL query, or calling an API, for example.

Even data that has previously been inserted in your database safely can introduce SQL injection if you use it wrong.

Bill Karwin
  • 538,548
  • 86
  • 673
  • 828
  • This is a good answer. A good additional is how people should safely add conditionals to SQL; and that's using things like `switch` statements so that user given values never touch the raw SQL string. Also in some situations it's better that *all* the data is passed back from MySQL to then be sorted (*ordered*) by the end point. `:-)` – Martin Jul 29 '19 at 16:48
  • Example: `switch ($_GET['order']){ case "desc": $sqlOrder = "DESC"; break; case "asc": $sqlOrder = "ASC"; break;}` so the `$_GET` value **NEVER** touches the SQL. – Martin Jul 29 '19 at 16:50
  • So you mean with switch never allows the text to tinker with my SQL? – Gaurav Mall Jul 29 '19 at 17:07
  • Keep in mind that implementation of prepared statements is different from one API to another. For example PDO and WordPress use emulated prepares by default, which can protect from SQL injection, but only when used properly. The duplicate question has explained in more detail how to set up a PDO connection in PHP to protect from SQL injection. – Dharman Jul 29 '19 at 17:52
  • 2
    Yes, PDO has "emulated" prepared statements which change the rules. There's no reason to use emulated prepared statements unless you use a database that doesn't support real prepared statements. – Bill Karwin Jul 29 '19 at 18:31