20

Will mysql_real_rescape_string() be enough to protect me from hackers and SQL attacks? Asking because I heard that these don't help against all attack vectors? Looking for the advice of experts.

EDIT: Also, what about LIKE SQL attacks?

Michael Berkowski
  • 267,341
  • 46
  • 444
  • 390
Monica
  • 1,585
  • 3
  • 23
  • 34
  • possible duplicate of [In PHP when submitting strings to the DB should I take care of illegal characters using htmlspecialchars() or use regex?](http://stackoverflow.com/questions/2993027/in-php-when-submitting-strings-to-the-db-should-i-take-care-of-illegal-characters) – Your Common Sense Mar 25 '11 at 14:23
  • LIKE attacks are not a big deal. There can be no harm from LIKE unless it's improperly used. Just do not use LIKE instead of `=` and you'll be okay. – Your Common Sense Mar 25 '11 at 14:26
  • possible duplicate of [Best way to prevent SQL Injection in PHP](http://stackoverflow.com/questions/60174/best-way-to-prevent-sql-injection-in-php) – tereško Sep 11 '12 at 00:58
  • 1
    related: [SQL injection that gets around mysql_real_escape_string()](http://stackoverflow.com/q/5741187/727208) – tereško Sep 11 '12 at 01:00

5 Answers5

14

@Charles is extremely correct!

You put yourself at risk for multiple types of known SQL attacks, including, as you mentioned

  • SQL injection: Yes! Mysql_Escape_String probably STILL keeps you susceptible to SQL injections, depending on where you use PHP variables in your queries.

Consider this:

$sql = "SELECT number FROM PhoneNumbers " .
       "WHERE " . mysql_real_escape_string($field) . " = " . mysql_real_escape_string($value);  

Can that be securely and accurately escaped that way? NO! Why? because a hacker could very well still do this:

Repeat after me:

mysql_real_escape_string() is only meant to escape variable data, NOT table names, column names, and especially not LIMIT fields.

  • LIKE exploits: LIKE "$data%" where $data could be "%" which would return ALL records ... which can very well be a security exploit... just imagine a Lookup by last four digits of a credit card... OOPs! Now the hackers can potentially receive every credit card number in your system! (BTW: Storing full credit cards is hardly ever recommended!)

  • Charset Exploits: No matter what the haters say, Internet Explorer is still, in 2011, vulnerable to Character Set Exploits, and that's if you have designed your HTML page correctly, with the equivalent of <meta name="charset" value="UTF-8"/>! These attacks are VERY nasty as they give the hacker as much control as straight SQL injections: e.g. full.

Here's some example code to demonstrate all of this:

// Contains class DBConfig; database information.
require_once('../.dbcreds');                       

$dblink = mysql_connect(DBConfig::$host, DBConfig::$user, DBConfig::$pass);
mysql_select_db(DBConfig::$db);
//print_r($argv);

$sql = sprintf("SELECT url FROM GrabbedURLs WHERE %s LIKE '%s%%' LIMIT %s",
               mysql_real_escape_string($argv[1]),
               mysql_real_escape_string($argv[2]),
               mysql_real_escape_string($argv[3]));
echo "SQL: $sql\n";
$qq = mysql_query($sql);
while (($data = mysql_fetch_array($qq)))
{
        print_r($data);
}

Here's the results of this code when various inputs are passed:

$ php sql_exploits.php url http://www.reddit.com id
SQL generated: SELECT url FROM GrabbedURLs 
               WHERE url LIKE 'http://www.reddit.com%'
               ORDER BY id;
Returns: Just URLs beginning w/ "http://www.reddit.com"

$ php sql_exploits.php url % id
SQL generated: SELECT url FROM GrabbedURLs 
               WHERE url LIKE '%%' 
               ORDER BY id;
Results: Returns every result Not what you programmed, ergo an exploit --

$ php sql_exploits.php 1=1 'http://www.reddit.com' id Results: Returns every column and every result.

Then there are the REALLLY nasty LIMIT exploits:

$ php sql_exploits.php url 
> 'http://www.reddit.com'
> "UNION SELECT name FROM CachedDomains"
Generated SQL: SELECT url FROM GrabbedURLs 
               WHERE url LIKE 'http://reddit.com%' 
               LIMIT 1 
               UNION
               SELECT name FROM CachedDomains;
Returns:  An entirely unexpected, potentially (probably) unauthorized query
          from another, completely different table. 

Whether you understand the SQL in the attacks or not is irrevelant. What this has demonstrated is that mysql_real_escape_string() is easily circumvented by even the most immature of hackers. That is because it is a REACTIVE defense mechism. It only fixes very limited and KNOWN exploits in the Database.

All escaping will NEVER be sufficient to secure databases. In fact, you can explicitly REACT to every KNOWN exploit and in the future, your code will most likely become vulnerable to attacks discovered in the future.

The proper, and only (really) , defense is a PROACTIVE one: Use Prepared Statements. Prepared statements are designed with special care so that ONLY valid and PROGRAMMED SQL is executed. This means that, when done correctly, the odds of unexpected SQL being able to be executed are drammatically reduced.

Theoretically, prepared statements that are implemented perfectly would be impervious to ALL attacks, known and unknown, as they are a SERVER SIDE technique, handled by the DATABASE SERVERS THEMSELVES and the libraries that interface with the programming language. Therefore, you're ALWAYS guaranteed to be protected against EVERY KNOWN HACK, at the bare minimum.

And it's less code:

$pdo = new PDO($dsn);

$column = 'url';
$value = 'http://www.stackoverflow.com/';
$limit = 1;

$validColumns = array('url', 'last_fetched');

// Make sure to validate whether $column is a valid search parameter.
// Default to 'id' if it's an invalid column.
if (!in_array($column, $validColumns) { $column = 'id'; }


$statement = $pdo->prepare('SELECT url FROM GrabbedURLs ' .
                           'WHERE ' . $column . '=? ' .
                           'LIMIT ' . intval($limit));
$statement->execute(array($value));
while (($data = $statement->fetch())) { }

Now that wasn't so hard was it? And it's forty-seven percent less code (195 chars (PDO) vs 375 chars (mysql_). That's what I call, "full of win".

EDIT: To address all the controversy this answer stirred up, allow me to reiterate what I have already said:

Using prepared statements allows one to harness the protective measures of the SQL server itself, and therefore you are protected from things that the SQL server people know about. Because of this extra level of protection, you are far safer than by just using escaping, no matter how thorough.

BMDan
  • 282
  • 5
  • 12
Theodore R. Smith
  • 21,848
  • 12
  • 65
  • 91
  • 1
    do not beg for the rep, it's disgusting. Especially if you spent so much time on such widely known subject. – Your Common Sense Mar 25 '11 at 14:11
  • 2
    and your prepared statements example is just ridiculous :) Did you happen to run it by chance? – Your Common Sense Mar 25 '11 at 14:21
  • @Col. Shrapnel: Thank you very much for your constructive criticism. I believe I have fixed all the bugs you have found. Thanks. – Theodore R. Smith Mar 25 '11 at 14:29
  • what if I search for just one letter `a` using like? and then b and so on? What's wrong with statement that's **intended** to return multiple results from the whole table? – Your Common Sense Mar 25 '11 at 14:33
  • 1
    your definition is still wrong. mysql_real_escape_string() is only meant to escape **strings** – Your Common Sense Mar 25 '11 at 14:34
  • @Col. Shrapnel, 1) I'm not interested in "what if the coder meant to select everything", i'm saying "what if you only want to allow searches for 'a%' and a hacker can get everything, instead?"... Even if you cannot conceive how this could be horrible, it doesn't mean it "isn't a big deal" (per your earlier comment). 2) Most people misinterpret what mysql_real_escape_string escapes... look at all the "yes" answers here... I'm glad you figured it out, but most haven't. – Theodore R. Smith Mar 25 '11 at 14:43
  • 2
    Data for `LIKE` expression should be sanitized additionally by escaping `%` and `_`. – zerkms Mar 25 '11 at 14:46
  • 1
    @Theodore R. Smith: so you're trying to say that I misinterpret what mysql_real_escape_string does?! – zerkms Mar 25 '11 at 14:47
  • 6
    @Theodore R. Smith: "All escaping will NEVER be sufficient to secure databases." --- well, can you **prove** that? Without a lot of theory and loud words. I create a script that uses mysql_real_escape_string and you're using any kind of sql injection to get protected data. Ok? Are you enough competent to do something real and more than just philosophy? – zerkms Mar 25 '11 at 14:50
  • 2
    what a pity. you don't get the point. it's not "coder meant". it's "LANGUAGE OPERATOR meant". It's operator meant to return multiple results from the whole table, so, a coder have to EXPECT it. If only one match is expected, NO LIKE should be used. that's all. and mysql_real_escape_string() will not help you with VARIABLE but of INT type. you refuse to learn further, it saddens me. That's probably because you're still making a big deal of your "research" and being too proud of yourself to listen. – Your Common Sense Mar 25 '11 at 14:52
  • 3
    and you corrected limit in the wrong way. PDO is okay with limit parameters, you can (and should) use prepared statement for this. in fact, your query assembling code looks no better than regular mysql one:) – Your Common Sense Mar 25 '11 at 14:55
  • +1 for describing the various SQL attacks, and showing a work-around that uses the SQL server's own protection logic instead of having to totally rely upon my own. Thank you. – Monica Mar 25 '11 at 15:43
  • @Col. Shrapnel. I thought you were being constructive with your observations on LIKE. Instead, now I see you're just a nit-picking asshole. – Theodore R. Smith Mar 25 '11 at 15:48
  • 2
    @Monica that's misleading answer. if you gonna protect your query this way `$id = mysql_real_escape_string($_GET['id]);$sql = "SELECT * FROM table WHERE id=$id"`, you'll be hacked. Good Theo call it nit-picking but I'd call it SQL injection. This guy never used code he wrote in practice. That's BIG problem. – Your Common Sense Mar 25 '11 at 16:41
  • @Col. Shrapnel: Go back and read what I actually wrote. *I* am the one who said that that would lead to SQL injection. It was PROOF that mysql_real_escape_string() by itself DOES NOT PROTECT from SQL injections. See now? We're on the same page, apparently. – Theodore R. Smith Mar 25 '11 at 20:40
  • 1
    @Theodore R. Smith: you did not answer to me. Can **you** prove your words? – zerkms Mar 26 '11 at 00:13
  • @zerkms You are trying to entrap me by placing words in my mouth which I never uttered. This is a classical logical fallacy and a sign that you are not interested, truly, in the truth, but just in proving a point. ... With that said, let me reiterate what I have already said: **Using prepared statements allows one to harness the protective measures of the SQL server itself, and therefore you are protected from things that the SQL server people know about. Because of this extra level of protection, you are far safer than by just using escaping, no matter how thorough.** – Theodore R. Smith Mar 26 '11 at 10:38
  • 2
    @Theodore R. Smith: sure, prepared statements are "safer". But that doesn't mean that using `mysql_real_rescape_string` is not safe. You still did not give any proof that `mysql_real_rescape_string` is not safe. I ask you again: let I write a script with `mysql_real_rescape_string` and you hack it by using any possible kind of vulnerabilty? Do you agree or your words mean nothing? – zerkms Mar 26 '11 at 11:40
  • Using `mysql_real_escape_string` in 2012 just shows one doesn't know the value and merits of using prepared statements. – Theodore R. Smith Apr 23 '12 at 13:20
8

No!


Important update: After testing possible exploit code provided by Col. Shrapnel and reviewing MySQL versions 5.0.22, 5.0.45, 5.0.77, and 5.1.48, it seems that the GBK character set and possibly others combined with a MySQL version lower than 5.0.77 may leave your code vulnerable if you only use SET NAMES instead of using the specific mysql_set_charset/mysqli_set_charset functions. Because those were only added in PHP 5.2.x, the combination of old PHP and old MySQL can yield a potential SQL injection vulnerability, even if you thought you were safe and did everything correctly, by-the-book.


Without setting the character set in combination with mysql_real_escape_string, you may find yourself vulnerable to a specific character set exploit possible with older MySQL versions. More info on previous research.

If possible, use mysql_set_charset. SET NAMES ... is not enough to protect against this specific exploit if you are using an effected version of MySQL (prior to 5.0.22 5.0.77).

Community
  • 1
  • 1
Charles
  • 50,943
  • 13
  • 104
  • 142
  • +1 This is an accurate answer and names one additional exploit and how to fix it. See my detailed answer below for all of the currently known SQL exploits, how `mysql_real_escape_string` is insufficient for each, and the safe and easy alternative: PDO prepared statements. – Theodore R. Smith Mar 25 '11 at 14:18
  • -1 for not mentioning that all this crap can happen only when extremely rare encoding is used, but no harm for utf and single-byte encodings. – Your Common Sense Mar 25 '11 at 14:38
  • @Col, said explanation of the extremely rare problem is linked in the post, and I *did* detail that it's a very specific exploit linked to an older version of MySQL. – Charles Mar 25 '11 at 15:11
  • lol, you didn't even get the point. SET NAMES is still not enough with any mysql version – Your Common Sense Mar 25 '11 at 15:19
  • @Col, please provide a query and user input that, when passed through `mysql_real_escape_string` after using `SET NAMES` that demonstrates the vulnerability in MySQL versions *newer* than 5.0.22. Or, put another way, put up or shut up. You keep ragging on people for "not understanding" and "missing the point" but never actually explain what you know that seemingly nobody else does. This is your chance. – Charles Mar 25 '11 at 15:33
  • @Col, thanks for the link. I am unable to replicate the actual injection with PHP 5.3.3 and MySQL 5.0.77 -- it's throwing a syntax error for the first two queries after escaping the not-backslash. Given the PHP version I'm using here is identical to the one you originally used, the difference could either be in MySQL or in the underlying driver. Did you use `mysqlnd`? My version was compiled against the official MySQL library. – Charles Mar 25 '11 at 15:52
  • @Col: `Notice: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '/*' AND password = 'guess'' at line 1SELECT * FROM users WHERE username = '¿\' OR username = username /*' AND password = 'guess' in /home/charles/public_html/sqli.php on line 16` -- the line number is off because I added a header() to set the HTML charset to Windows-1252 and some HRs between sections. – Charles Mar 25 '11 at 16:10
  • @Col, I'm also unable to dupe using PHP 5.0.4 (icky) and MySQL 5.1.48. Same syntax error twice, then it dies because `mysql_set_charset` is a 5.2 thing. – Charles Mar 25 '11 at 16:11
  • @Col, the only thing in common between the two PHP & MySQL installs is that they're both compiled against the official library. Can you check to see if your afflicted version uses `mysqlnd` instead? If so, I think that's where the bug might be... and in that case, it might actually be fixable by the PHP team. – Charles Mar 25 '11 at 16:13
  • @Col, I *have* verified the problem using the PHP 5.1.6 and MySQL 5.0.22 (!!!) shipped in vanilla, un-updated CentOS 5.0. I'm going to see if I can find an intermediary version (< 5.0.77) to test. I might just be able to grab the 5.0.45 that shipped with CentOS 5.2. – Charles Mar 25 '11 at 18:51
  • @Col, the spamming continues. 5.0.45 as shipped with CentOS 5.2 and 5.3 is vulnerable. 5.0.77 as shipped with CentOS 5.4 and later *is not* vulnerable. I guess it's time to go changelog diving. – Charles Mar 25 '11 at 18:56
  • I was unable to find any appropriate mentions of character set changes or GBK handling changes in "community server" releases between 5.0.45 and 5.0.77. So, whatever got fixed was either done as part of an unrelated fix, or was by accident. There were *lots* of character set tweaks, and a handful of GBK fixes in the 5.0.51 timeframe. The only thing I can conclude is that **anything prior to 5.0.77 may be unsafe**. I'm going to update my answer to reflect this. – Charles Mar 25 '11 at 19:16
  • @Charles, some people are quite antagonistic in this post. You obviously held your ground well, and I liked your answer and would want it to be the primary one if I hadn't responded. Let's not feed the trolls, shall we? – Theodore R. Smith Mar 26 '11 at 10:42
  • @Theodore, if not for his responses, I could not have updated my answer with more information on when `mysql_real_escape_string` isn't enough. The Colonel may have been antagonistic here, but I'm not sure he's a troll. – Charles Mar 26 '11 at 15:55
7

Yes. If you will not forget to:

  1. Escape string data with mysql_real_rescape_string()
  2. Cast numbers to numbers explicitly (ie: $id = (int)$_GET['id'];)

then you're protected.

zerkms
  • 249,484
  • 69
  • 436
  • 539
  • +1 And if you forget 2, and use `mysql_real_escape_string()`, always quote them :) – alex Mar 24 '11 at 04:31
  • @alex sometimes you can't quite them – Your Common Sense Mar 25 '11 at 14:17
  • 1
    -1 This is very dangerous bad advice. I don't mean to be mean – you probably don't know any better – but there is substantially more to SQL security than merely escaping input variables...and a much easier and safer system, which I describe (in detail) below. – Theodore R. Smith Mar 25 '11 at 14:17
  • 2
    @Theodore R. Smith: well, in my practice I never take field or table names from users' input. Yes, the query can depend on the input, but I never put anything but data right into sql. So, I think my advice is still good and sexy. – zerkms Mar 25 '11 at 14:43
  • @Col. Shrapnel: have no idea why to take identifiers from request as-is. Never had such tasks. – zerkms Mar 25 '11 at 14:59
  • but it isn't you but the OP asking this, so, it's good idea to mention identifiers *to share your habit of not placing identifiers as-is* – Your Common Sense Mar 25 '11 at 15:29
3

I personally prefer prepared statements:

<?php
$stmt = $dbh->prepare("SELECT * FROM REGISTRY where name = ?");
if ($stmt->execute(array($_GET['name']))) {
  while ($row = $stmt->fetch()) {
    print_r($row);
  }
}
?>

It would be pretty easy to overlook one or another specific variable that has been missed when using one of the *escape_string() functions, but if all your queries are prepared statements, then they are all fine, and use of interpolated variables will stand out like a sore thumb.

But this is far from sufficient to ensure you're not vulnerable to remote exploits: if you're passing around an &admin=1 with GET or POST requests to signify that someone is an admin, every one of your users could easily upgrade their privileges with two or three seconds of effort. Note that this problem isn't always this obvious :) but this is an easy way to explain the consequences of trusting user-supplied input too much.

sarnold
  • 102,305
  • 22
  • 181
  • 238
2

You should look into using prepared statements/parameterized queries instead. The idea is that you give the database a query with placeholders. You then give the database your data, and tell it which placeholder to replace with said data, and the database makes sure that it's valid and doesn't allow it to overrun the placeholder (i.e. it can't end a current query and then add its own - a common attack).

AgentConundrum
  • 20,288
  • 6
  • 64
  • 99
  • +1 This is an accurate answer and names the primary method on how to prevent SQL exploits. See my detailed answer below for all of the currently known SQL exploits, how `mysql_real_escape_string` is insufficient for each, and @AgentConundrum's safe and easy alternative: PDO prepared statements. – Theodore R. Smith Mar 25 '11 at 14:19