736

Let's say I have code like this:

$dbh = new PDO("blahblah");

$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

The PDO documentation says:

The parameters to prepared statements don't need to be quoted; the driver handles it for you.

Is that truly all I need to do to avoid SQL injections? Is it really that easy?

You can assume MySQL if it makes a difference. Also, I'm really only curious about the use of prepared statements against SQL injection. In this context, I don't care about XSS or other possible vulnerabilities.

Patrick Hofman
  • 153,850
  • 22
  • 249
  • 325
Mark Biek
  • 146,731
  • 54
  • 156
  • 201

7 Answers7

916

The short answer is YES, PDO prepares are secure enough if used properly.


I'm adapting this answer to talk about PDO...

The long answer isn't so easy. It's based off an attack demonstrated here.

The Attack

So, let's start off by showing the attack...

$pdo->query('SET NAMES gbk');
$var = "\xbf\x27 OR 1=1 /*";
$query = 'SELECT * FROM test WHERE name = ? LIMIT 1';
$stmt = $pdo->prepare($query);
$stmt->execute(array($var));

In certain circumstances, that will return more than 1 row. Let's dissect what's going on here:

  1. Selecting a Character Set

    $pdo->query('SET NAMES gbk');
    

    For this attack to work, we need the encoding that the server's expecting on the connection both to encode ' as in ASCII i.e. 0x27 and to have some character whose final byte is an ASCII \ i.e. 0x5c. As it turns out, there are 5 such encodings supported in MySQL 5.6 by default: big5, cp932, gb2312, gbk and sjis. We'll select gbk here.

    Now, it's very important to note the use of SET NAMES here. This sets the character set ON THE SERVER. There is another way of doing it, but we'll get there soon enough.

  2. The Payload

    The payload we're going to use for this injection starts with the byte sequence 0xbf27. In gbk, that's an invalid multibyte character; in latin1, it's the string ¿'. Note that in latin1 and gbk, 0x27 on its own is a literal ' character.

    We have chosen this payload because, if we called addslashes() on it, we'd insert an ASCII \ i.e. 0x5c, before the ' character. So we'd wind up with 0xbf5c27, which in gbk is a two character sequence: 0xbf5c followed by 0x27. Or in other words, a valid character followed by an unescaped '. But we're not using addslashes(). So on to the next step...

  3. $stmt->execute()

    The important thing to realize here is that PDO by default does NOT do true prepared statements. It emulates them (for MySQL). Therefore, PDO internally builds the query string, calling mysql_real_escape_string() (the MySQL C API function) on each bound string value.

    The C API call to mysql_real_escape_string() differs from addslashes() in that it knows the connection character set. So it can perform the escaping properly for the character set that the server is expecting. However, up to this point, the client thinks that we're still using latin1 for the connection, because we never told it otherwise. We did tell the server we're using gbk, but the client still thinks it's latin1.

    Therefore the call to mysql_real_escape_string() inserts the backslash, and we have a free hanging ' character in our "escaped" content! In fact, if we were to look at $var in the gbk character set, we'd see:

    縗' OR 1=1 /*

    Which is exactly what the attack requires.

  4. The Query

    This part is just a formality, but here's the rendered query:

    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
    

Congratulations, you just successfully attacked a program using PDO Prepared Statements...

The Simple Fix

Now, it's worth noting that you can prevent this by disabling emulated prepared statements:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

This will usually result in a true prepared statement (i.e. the data being sent over in a separate packet from the query). However, be aware that PDO will silently fallback to emulating statements that MySQL can't prepare natively: those that it can are listed in the manual, but beware to select the appropriate server version).

The Correct Fix

The problem here is that we used SET NAMES instead of C API's mysql_set_charset(). Otherwise, the attack would not succeed. But the worst part is that PDO didn't expose the C API for mysql_set_charset() until 5.3.6, so in prior versions it cannot prevent this attack for every possible command! It's now exposed as a DSN parameter, which should be used instead of SET NAMES...

This is provided we are using a MySQL release since 2006. If you're using an earlier MySQL release, then a bug in mysql_real_escape_string() meant that invalid multibyte characters such as those in our payload were treated as single bytes for escaping purposes even if the client had been correctly informed of the connection encoding and so this attack would still succeed. The bug was fixed in MySQL 4.1.20, 5.0.22 and 5.1.11.

The Saving Grace

As we said at the outset, for this attack to work the database connection must be encoded using a vulnerable character set. utf8mb4 is not vulnerable and yet can support every Unicode character: so you could elect to use that instead—but it has only been available since MySQL 5.5.3. An alternative is utf8, which is also not vulnerable and can support the whole of the Unicode Basic Multilingual Plane.

Alternatively, you can enable the NO_BACKSLASH_ESCAPES SQL mode, which (amongst other things) alters the operation of mysql_real_escape_string(). With this mode enabled, 0x27 will be replaced with 0x2727 rather than 0x5c27 and thus the escaping process cannot create valid characters in any of the vulnerable encodings where they did not exist previously (i.e. 0xbf27 is still 0xbf27 etc.)—so the server will still reject the string as invalid. However, see @eggyal's answer for a different vulnerability that can arise from using this SQL mode (albeit not with PDO).

Safe Examples

The following examples are safe:

mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Because the server's expecting utf8...

mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Because we've properly set the character set so the client and the server match.

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Because we've turned off emulated prepared statements.

$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Because we've set the character set properly.

$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

Because MySQLi does true prepared statements all the time.

Wrapping Up

If you:

  • Use Modern Versions of MySQL (late 5.1, all 5.5, 5.6, etc) AND PDO's DSN charset parameter (in PHP ≥ 5.3.6)

OR

  • Don't use a vulnerable character set for connection encoding (you only use utf8 / latin1 / ascii / etc)

OR

  • Enable NO_BACKSLASH_ESCAPES SQL mode

You're 100% safe.

Otherwise, you're vulnerable even though you're using PDO Prepared Statements...

Addendum

I've been slowly working on a patch to change the default to not emulate prepares for a future version of PHP. The problem that I'm running into is that a LOT of tests break when I do that. One problem is that emulated prepares will only throw syntax errors on execute, but true prepares will throw errors on prepare. So that can cause issues (and is part of the reason tests are borking).

Dharman
  • 30,962
  • 25
  • 85
  • 135
ircmaxell
  • 163,128
  • 34
  • 264
  • 314
  • I think "The Saving Grace" was this MySQL 5.0.22 fix: http://dev.mysql.com/doc/relnotes/mysql/5.0/en/news-5-0-22.html – nico gawenda Apr 17 '13 at 12:23
  • 1
    @nicogawenda: that was a different bug. Prior to 5.0.22, `mysql_real_escape_string` wouldn't properly handle cases where the connection was properly set to BIG5/GBK. So actually even calling `mysql_set_charset()` on mysql < 5.0.22 would be vulnerable to this bug! So no, this post is still applicable to 5.0.22 (because mysql_real_escape_string is only charset away to calls from `mysql_set_charset()`, which is what this post is talking about bypassing)... – ircmaxell Apr 17 '13 at 14:10
  • @ircmaxell. Thanks, this is great stuff. One question left for me, concerning the setting the emulation to false. Do I need to have that $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); in every query or should it be enough once in a script? Thanks. – BernardA Dec 21 '13 at 17:29
  • 4
    Please note that `NO_BACKSLASH_ESCAPES` can also introduce new vulnerabilities : http://stackoverflow.com/a/23277864/1014813 – lepix Jan 03 '16 at 17:22
  • 1
    @ircmaxell A little late to the party, but I am no expert, I am really trying to learn new stuff and I cannot get how and why `SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1` is a successful attack. To me it looks like that is searching for something that is not there. The comment is the attack? It leaves everyhting hanging? Maybe you could add a couple of lines explaining this part, for noobs like me. Thank you for the amazing answer. – slevin Jan 24 '18 at 21:59
  • 3
    @slevin the "OR 1=1" is a placeholder for whatever you want. Yes, it's searching for a value in name, but imagine the "OR 1=1" part was "UNION SELECT * FROM users". You now control the query, and as such can abuse it... – ircmaxell Jan 25 '18 at 20:17
  • I can't reproduce this attack using PHP v7.2.0 with manually setting `charset=latin1`. Thrown error is #42000, stating `...near '/*' LIMIT 1' at line 1"`. Is it supposed to be like that? – revo Jun 20 '18 at 19:04
  • @Your What bugs are you talking about? This attack is [still possible](https://phpize.online/sql/mariadb/54a9fdd471d9a6767cf4f116ecd1829c/php/php81/fbcceb24b0eb8117cbb4241ec9d4afc9/) to this day. It isn't something that was fixed in PHP or in MySQL because it isn't really a bug. – Dharman Sep 28 '22 at 18:52
  • @Dharman those mentioned in the post, [this](https://bugs.mysql.com/bug.php?id=8378) or that prior 5.3.6 DSN issue. Though I don't insist on the phrasing, it can be anything as long as this post provides actual up to date information and doesn't scare newbies – Your Common Sense Sep 28 '22 at 19:12
  • @YourCommonSense Yes, but to avail of that fix you have to be aware that the problem exists. The issue was solved by adding `set_charset` to mysqli and `charset` option to PDO's DSN, but if you don't set it properly, as demonstrated on phpize, you can still be vulnerable to SQL injection. – Dharman Sep 28 '22 at 19:16
  • @revo Yes, that is the attack. If the attack was unsuccessful then you should not see any error message. – Dharman Sep 28 '22 at 19:57
523

Prepared statements / parameterized queries are sufficient to prevent SQL injections, but only when used all the time, for the every query in the application.

If you use un-checked dynamic SQL anywhere else in an application it is still vulnerable to 2nd order injection.

2nd order injection means data has been cycled through the database once before being included in a query, and is much harder to pull off. AFAIK, you almost never see real engineered 2nd order attacks, as it is usually easier for attackers to social-engineer their way in, but you sometimes have 2nd order bugs crop up because of extra benign ' characters or similar.

You can accomplish a 2nd order injection attack when you can cause a value to be stored in a database that is later used as a literal in a query. As an example, let's say you enter the following information as your new username when creating an account on a web site (assuming MySQL DB for this question):

' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '

If there are no other restrictions on the username, a prepared statement would still make sure that the above embedded query doesn't execute at the time of insert, and store the value correctly in the database. However, imagine that later the application retrieves your username from the database, and uses string concatenation to include that value a new query. You might get to see someone else's password. Since the first few names in users table tend to be admins, you may have also just given away the farm. (Also note: this is one more reason not to store passwords in plain text!)

We see, then, that if prepared statements are only used for a single query, but neglected for all other queries, this one query is not sufficient to protect against sql injection attacks throughout an entire application, because they lack a mechanism to enforce all access to a database within an application uses safe code. However, used as part of good application design — which may include practices such as code review or static analysis, or use of an ORM, data layer, or service layer that limits dynamic sql — **prepared statements are the primary tool for solving the Sql Injection problem.** If you follow good application design principles, such that your data access is separated from the rest of your program, it becomes easy to enforce or audit that every query correctly uses parameterization. In this case, sql injection (both first and second order) is completely prevented.


*It turns out that MySql/PHP were (long, long time ago) just dumb about handling parameters when wide characters are involved, and there was a rare case outlined in the other highly-voted answer here that can allow injection to slip through a parameterized query.

Joel Coehoorn
  • 399,467
  • 113
  • 570
  • 794
  • 6
    That's interesting. I wasn't aware of 1st order vs. 2nd order. Can you elaborate a little more on how 2nd order works? – Mark Biek Sep 25 '08 at 15:54
  • 197
    If ALL your queries are parametrized, you're also protected against 2nd order injection. 1st order injection is forgetting that user data is untrustworthy. 2nd order injection is forgetting that database data is untrustworthy (because it came from the user originally). – cjm Sep 25 '08 at 16:00
  • 6
    Thanks cjm. I also found this article helpful in explaining 2nd order injections: http://www.codeproject.com/KB/database/SqlInjectionAttacks.aspx – Mark Biek Sep 25 '08 at 16:03
  • 50
    Ah, yes. But what about **third order injection**. Have to be aware of those. – troelskn Jun 28 '11 at 10:05
  • 5
    Yes, except that PDO doesn't use real prepared statements by default ;-). See my answer below for a demonstration and explanation of an attack... – ircmaxell Aug 30 '12 at 17:23
  • 83
    @troelskn that must be where the developer is the source of untrustworthy data – MikeMurko Nov 03 '12 at 02:03
  • @JoelCoehoorn Think you'll find most are coming from here ;) http://www.google.com.au/search?q=pdo+prepared+statements – jduncanator Sep 29 '13 at 05:24
  • But if you use 100% static prepared statements in your whole program, I mean, all SQL hardcoded into prepared statements, then you avoid all levels of injection, right? – StormByte Feb 07 '14 at 09:11
  • @StormByte Yes, you do, but you're simply avoiding client data. – DanRedux May 05 '14 at 16:41
  • 5
    I wasn't aware of the order thingy. Reading this I thought _What about 3rd and 4th and even more of 5th?? And 6th must be so deadly..._ – user3459110 May 12 '14 at 13:17
  • 1
    @Pacerier Joel made an edit after my comment on March 8th, emphasizing a sentence in what he already wrote. It's clear that the post has the content he intends. I'm not going to edit someone's post to change the meaning of it, and other answers already answer the question. – Brad Apr 11 '15 at 16:18
  • 1
    @Pacerier I disagree with you there. The system is set up in a way that we can edit everone's posts, yes, but it is good etiquette and rule to not drastically change the original meaning. – Brad Apr 12 '15 at 16:45
  • 1
    @Brad, That's provided the "original meaning" isn't wrong. If it's wrong, feel free to edit it to make it right. – Pacerier Apr 12 '15 at 18:25
  • And also in 2nd order injection the attacker have to put the names of tables and columns that bring the blind sql injection in... Well,What I do is this.. $username=strip_tags(mysqli_real_escape_string($con,$username)); after length verification.. and then i also use reg exp to verify and in end put it by PDO – Waqas Tahir Jun 10 '15 at 11:11
  • 5
    @Waqas Then you have all kinds of problems. What if those tags are valid and need to be stored in the DB? Why use reg exp *after* real_escape? User input shouldn't reach DB sanitisation *before* validation has passed. Why are you doing `mysqli_real_escape_string` and then PDO? Sure, your final PDO makes it secure *if* you use parametrised statements, but all the extra bits of (pointless) "security" you have added are more likely to introduce a security flaw away from DB security, or at the very least nightmare code which is terrible to understand and work with. – James Jun 20 '15 at 14:07
  • I don't want to allow the user to put tags in db and please can you tell me what means by "if those tags are valid...." and i have made all these things into one function called validate which i have in my own framework file of website that i use to make my code responsive and cool – Waqas Tahir Jun 26 '15 at 15:16
  • @Waqas don't validate input... **quarantine** it. That's what query parameters do. If you're trying to protect from something other than sql injection, you need to look in a different place. – Joel Coehoorn Jun 26 '15 at 15:57
  • So “second-order injection” is that moment when people figure out that in-database data should not be trusted anymore than query parameters eh? I see Dunning-Kruger is alive and well in the computer security discipline (as it always was). – DomQ Jun 25 '19 at 17:30
  • `' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '` generally we apply select over columns, not on the column values. If this is stored as a username, the db will still return the result set with this as the content of one of the rows.. We do use them in conditions though. Please correct me. Thanks @JoelCoehoorn – Danyal Sandeelo Feb 19 '20 at 14:53
  • Higher order (n > 10) injections are now available at the market. – Ömer An Jul 05 '21 at 08:41
45

No, they are not always.

It depends on whether you allow user input to be placed within the query itself. For example:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

would be vulnerable to SQL injections and using prepared statements in this example won't work, because the user input is used as an identifier, not as data. The right answer here would be to use some sort of filtering/validation like:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];
$allowedTables = array('users','admins','moderators');
if (!in_array($tableToUse,$allowedTables))    
 $tableToUse = 'users';

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

Note: you can't use PDO to bind data that goes outside of DDL (Data Definition Language), i.e. this does not work:

$stmt = $dbh->prepare('SELECT * FROM foo ORDER BY :userSuppliedData');

The reason why the above does not work is because DESC and ASC are not data. PDO can only escape for data. Secondly, you can't even put ' quotes around it. The only way to allow user chosen sorting is to manually filter and check that it's either DESC or ASC.

Tower
  • 98,741
  • 129
  • 357
  • 507
  • 12
    Am I missing something here but isn't the whole point of prepared statements to avoid treating sql like a string? Wouldn't something like $dbh->prepare('SELECT * FROM :tableToUse where username = :username'); get around your problem? – Rob Forrest Sep 25 '12 at 14:29
  • 4
    @RobForrest yes you are missing :). The data you bind only works for DDL (Data Definition Language). You need those quotes and proper escaping. Placing quotes for other parts of the query breaks it with a high probability. For example, `SELECT * FROM 'table'` can be wrong as it should be `SELECT * FROM \`table\`` or without any backsticks. Then some things like `ORDER BY DESC` where `DESC` comes from the user can't be simply escaped. So, practical scenarios are rather unlimited. – Tower Sep 26 '12 at 16:25
  • 11
    I wonder how 6 people could upvote a comment proposing a plainly wrong use of a prepared statement. Had they even tried it once, they'd have discovered right away that using named parameter in place of a table name will not work. – Félix Adriyel Gagnon-Grenier Apr 30 '14 at 14:08
  • Here is a great tutorial on PDO if you want to learn it. http://a2znotes.blogspot.in/2014/09/introduction-to-pdo.html – RN Kushwaha Sep 12 '14 at 17:08
  • 11
    You should never use a query string/POST body to pick the table to use. If you don't have models, at least use a `switch` to derive the table name. – ZiggyTheHamster Sep 26 '14 at 17:17
  • if you need to select table name dynamically there's always other tools like `sprintf`. Just construct a prepared statement template there, and then only prepare the generated template statement. – Jeffrey04 Feb 01 '18 at 08:12
  • @RobForrest I know this dates from a long time, but yes indeed, you are missing something :) That would not work (you could also have tried it). Preparing a statement means that the database engine will plan the queries. To do that, it needs to know *which tables* will be used, which columns will be selected, and so on. You can't parameterize a table name, it does not make sense. – Félix Adriyel Gagnon-Grenier May 11 '18 at 19:28
33

No this is not enough (in some specific cases)! By default PDO uses emulated prepared statements when using MySQL as a database driver. You should always disable emulated prepared statements when using MySQL and PDO:

$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Another thing that always should be done it set the correct encoding of the database:

$dbh = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');

Also see this related question: How can I prevent SQL injection in PHP?

Note that this will only protect you against SQL injection, but your application could still be vulnerable to other kinds of attacks. E.g. you can protect against XSS by using htmlspecialchars() again with the correct encoding and quoting style.

Dharman
  • 30,962
  • 25
  • 85
  • 135
PeeHaa
  • 71,436
  • 58
  • 190
  • 262
29

Yes, it is sufficient. The way injection type attacks work, is by somehow getting an interpreter (The database) to evaluate something, that should have been data, as if it was code. This is only possible if you mix code and data in the same medium (Eg. when you construct a query as a string).

Parameterised queries work by sending the code and the data separately, so it would never be possible to find a hole in that.

You can still be vulnerable to other injection-type attacks though. For example, if you use the data in a HTML-page, you could be subject to XSS type attacks.

troelskn
  • 115,121
  • 27
  • 131
  • 155
  • 11
    "Never" is **way** overstating it, to the point of being misleading. If you are using prepared statements incorrectly, it's not much better than not using them at all. (Of course, a "prepared statement" that has had user input injected into it defeats the purpose...but i've actually seen it done. And prepared statements can't handle identifiers (table names etc) as parameters.) Add to that, some of the PDO drivers *emulate* prepared statements, and there's room for them to do so incorrectly (for instance, by half-assedly parsing the SQL). Short version: never assume it is that easy. – cHao Jul 18 '12 at 08:31
15

Personally I would always run some form of sanitation on the data first as you can never trust user input, however when using placeholders / parameter binding the inputted data is sent to the server separately to the sql statement and then binded together. The key here is that this binds the provided data to a specific type and a specific use and eliminates any opportunity to change the logic of the SQL statement.

JimmyJ
  • 4,311
  • 3
  • 27
  • 25
-1

Eaven if you are going to prevent sql injection front-end, using html or js checks, you'd have to consider that front-end checks are "bypassable".

You can disable js or edit a pattern with a front-end development tool (built in with firefox or chrome nowadays).

So, in order to prevent SQL injection, would be right to sanitize input date backend inside your controller.

I would like to suggest to you to use filter_input() native PHP function in order to sanitize GET and INPUT values.

If you want to go ahead with security, for sensible database queries, I'd like to suggest to you to use regular expression to validate data format. preg_match() will help you in this case! But take care! Regex engine is not so light. Use it only if necessary, otherwise your application performances will decrease.

Security has a costs, but do not waste your performance!

Easy example:

if you want to double check if a value, received from GET is a number, less then 99 if(!preg_match('/[0-9]{1,2}/')){...} is heavyer of

if (isset($value) && intval($value)) <99) {...}

So, the final answer is: "No! PDO Prepared Statements does not prevent all kind of sql injection"; It does not prevent unexpected values, just unexpected concatenation

snipershady
  • 197
  • 1
  • 7