PDO::quote always seems to slap on two single quotes regardless of the type of value I pass it, or the parameter type I set.
e.g.,
$x = null;
echo $pdo->quote($x,PDO::PARAM_NULL); // ''
Thus I've extended the PDO class with my own function,
public function quote($value, $parameter_type=PDO::PARAM_STR) {
if(is_null($value)) return 'NULL';
elseif(is_bool($value)) return $value ? 'TRUE' : 'FALSE';
elseif(is_int($value)||is_float($value)) return $value;
return parent::quote($value, $parameter_type);
}
Have I missed any cases? Is there any harm in doing this?
Do the different parameter types ever do anything?
Per the documentation, the results of a call to quote() are dependent on the PDO driver, because different databases escape strings in different ways. So some drivers may require quotes in different places than others. But really, if you know you don't need quotes, then you shouldn't be calling quote().
As for your function, it depends on what you're trying to do. If you're trying to build a database-agnostic data access layer, then your function will break for some databases. This is because not all DBs have boolean types, e.g. there is no such value as TRUE in SQLite.
But really, it would be better to just use prepared statements and not touch quote() at all. Even the documentation for quote says so, after all.
Related
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
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.
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)
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.
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.
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.
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";
I've been using PDO::quote to generate SQL statements. I use the generated SQL to create a memcached key. I'd like to avoid having to create a connection solely for the purpose of escaping SQL values.
What is the purpose of needing to establish a connection to use PDO::quote method and can it be avoided?
Because PDO is an abstraction layer, and each database's quoting methods differ. PDO must know WHICH database you're going to be using to be able to use the proper quoting/escaping API calls.
You need a connection because the notion of escaping variables only makes sense when in the context of a database connection with respect to which they are to be escaped. Different characters might be interpreted differently, say, by MySQL and PostgreSQL, or depending on the character set of the connection. So it does not make sense to talk about escaping variables in a vacuum, you need some notion of how the resulting string will be interpreted.
Maybe you could use something else as a key to memcached.
If you know the driver that the SQL is going to be used with, just write a function of your own, e.g.
/**
* #param {string|int|null} $data
* #return string
*/
function quote ($data) {
if (is_float($data) || is_int($data)) {
return $data;
} else if (is_string($data)) {
return '\'' . addslashes($data) . '\'';
} else if ($data) {
throw new Exception('Invalid data type.');
} else {
return 'NULL';
}
}
An example use case: You have a CLI program that is used to generate SQL code that will be executed later by another program. In this scenario, establishing MySQL connection is redundant and might be unwanted.
It appears no matter what value/data-type pair I pass to $pdo->quote($value, $type);, it always quotes it as a string:
echo $pdo->quote('foo', PDO::PARAM_STR); /* 'foo', as expected */
echo $pdo->quote(42, PDO::PARAM_INT); /* '42', expected 42 unquoted */
I'm just curious to know if this is the intended functionality. I use prepared statements for actual query execution, but I'm trying to fetch create the final querystrings (for debugging/caching), and am constructing them manually.
As the title suggests, this is when $pdo is created using the MySQL driver. I haven't tried others due to unavailability.
The Oracle, SQLite, and Firebird drivers all quote as the PDO MySQL driver, ignoring the param type.
The MSSQL driver only checks the param type for whether it should use a national or regular character set (based on the PDO::PARAM_STR_NATL and PDO::PARAM_STR_CHAR flags); otherwise, it ignores the param type.
The PostgreSQL driver only distinguishes between binary large objects and all others.
The ODBC Driver doesn't implement a quoter.
The (lack of) behavior you expect was reported as a bug and closed as "bogus", meaning the behavior is by design. Perhaps the documentation is misleading when it states:
PDO::quote() places quotes around the input string (if required)
While this suggests there may be instances when values aren't surrounded by quotes, it doesn't say there definitely are, nor does it state what those instances are. If you feel this is a bug in documentation, submit a bug report, preferably with a fix.
public static function quote($value, $pdotype = PDO::PARAM_STR)
{
if ($pdotype == PDO::PARAM_INT)
return (int)$value;
return Db::pdo()->quote($value, $pdotype);
}
According to the PDO developers it's a intentional error in their code and in their documentation.
They do not seem to plan to correct it, so you can do it yourself by wrapping their errornous function and replacing the behaviour as needed.
You actually have no choice as in some cases you NEED a correct quote behaviour for numbers, you can't just use string quoting everywhere as SQL might just not take it.
As a sidenote, the above function will make a 0 out of any illegal data.
SQL injections are not possible but it will not throw an error.
If you want to catch errors you could do a "strlen" on both variables and if that differs you know there was a problem or an intrusion attempt.
Is there an easier way of safely extracting submitted variables other than the following?
if(isset($_REQUEST['kkld'])) $kkld=mysql_real_escape_string($_REQUEST['kkld']);
if(isset($_REQUEST['info'])) $info=mysql_real_escape_string($_REQUEST['info']);
if(isset($_REQUEST['freq'])) $freq=mysql_real_escape_string($_REQUEST['freq']);
(And: would you use isset() in this context?)
To escape all variables in one go:
$escapedGet = array_map('mysql_real_escape_string', $_GET);
To extract all variables into the current namespace (i.e. $foo = $_GET['foo']):
extract($escapedGet);
Please do not do this last step though. There's no need to, just leave the values in an array. Extracting variables can lead to name clashes and overwriting of existing variables, which is not only a hassle and a source of bugs but also a security risk. Also, as #BoltClock says, stick to $_GET or $_POST. Also2, as #zerkms points out, there's no point in mysql_real_escaping variables that are not supposed to be used in a database query, it may even lead to further problems.
Note that really none of this is a particularly good idea at all, you're just reincarnating magic_quotes and global_vars, which were horrible PHP practices from ages past. Use prepared statements with bound parameters via mysqli or PDO and use values through $_GET or filter_input. See http://www.phptherightway.com.
You can also use a recursive function like this to accomplish that
function sanitate($array) {
foreach($array as $key=>$value) {
if(is_array($value)) { sanitate($value); }
else { $array[$key] = mysql_real_escape_string($value); }
}
return $array;
}
sanitate($_POST);
To sanitize or validate any INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER, or INPUT_ENV, you can use
filter_input_array — Gets external variables and optionally filters them
Filtering can be done with a callback, so you could supply mysql_real_escape_string.
This method does not allow filtering for $_REQUEST, because you should not work with $_REQUEST when the data is available in any of the other superglobals. It's potentially insecure.
The method also requires you to name the input keys, so it's not a generic batch filtering. If you want generic batch filtering, use array_map or array_walk or array_filter as shown elsewhere on this page.
Also, why are you using the old mysql extension instead of the mysqli (i for improved) extension. The mysqli extension will give you support for transactions, multiqueries and prepared statements (which eliminates the need for escaping) All features that can make your DB code much more reliable and secure.
As far as I'm concerned Starx' and Ryan's answer from Nov 19 '10 is the best solution here as I just needed this, too.
When you have multiple input fields with one name (e.g. names[]), meaning they will be saved into an array within the $_POST-array, you have to use a recursive function, as mysql_real_escape_string does not work for arrays.
So this is the only solution to escape such a $_POST variable.
function sanitate($array) {
foreach($array as $key=>$value) {
if(is_array($value)) { sanitate($value); }
else { $array[$key] = mysql_real_escape_string($value); }
}
return $array;
}
sanitate($_POST);
If you use mysqli extension and you like to escape all GET variables:
$escaped_get = array_map(array($mysqli, 'real_escape_string'), $_GET);
As an alternative, I can advise you to use PHP7 input filters, which provides a shortcut to sql escaping. I'd not recommend it per se, but it spares creating localized variables:
$_REQUEST->sql['kkld']
Which can be used inline in SQL query strings, and give an extra warning should you forget it:
mysql_query("SELECT x FROM y WHERE z = '{$_REQUEST->sql['kkld']}'");
It's syntactically questionable, but allows you escaping only those variables that really need it. Or to emulate what you asked for, use $_REQUEST->sql->always();
I have a mysql query which requires that parameters be enclosed in either "" or '',
if I have an array passed to this function:
function orderbyfield($column, array $selection)
{
// will it be alright (secure) to do this?
foreach ($selection as $s)
{
$s = '"' . $s . '"';
}
$string = implode(',', $selection)
return array($column, $string);
}
and pass it to
function generate_sql()
{
$fields = $this->orderbyfield(); // assuming the code is in a class
$sql = 'SELECT FIELDS FROM TABLE ORDER BY FIELD (' . $fields[0] . ',' . mysql_real_escape_string($fields[1]));
}
will there be any security issues with this approach?
EDIT
assume that code is in a class, made necessary addition of $this->
EDIT
typo on the foreach
As others have said you should be using mysql_real_escape_string at the point where you create the query string. Also, although the database may be able to cast between types, not all the variables need to be quoted in queries:
function enclose($val, $dbh)
{
if (($val==='') || (is_null($val))) {
return 'NULL';
}
// is it a number?
if (preg_match('/^[\+-]*\d+\.?\d*$/', $val)) {
return($val);
}
// its a string
return("'" . mysql_real_escape_string($val, $dbh) . "'");
}
The null handling might need to be tweaked. (the above is cut down from a generic interface I use which also reads the structure of the table using DESCRIBE to get hints on when to quote/use nulls etc)
C.
Because you are using the mysql_real_escape_string function, it is pretty safe as far as strings are concerned. See dealing with sql injection for more info.
You should add quotes arround your string, but there quotes inside your strings themselves should also be escaped -- this can be done using mysql_real_escape_string, mysqli_real_escape_string, or PDO::quote, depending on the kind of functions/methods you are using to connect to your database.
Doing this (As you are already doing -- which is nice) should prevent SQL injections (at least for string : you should also check that numerics are indeed corresponding to numerical data, for instance)
Another solution, maybe a bit easier once you get it, would be to use Prepared statements.
See :
PDO::prepare
or mysqli_prepare
(Those can't be used with the old mysql_* functions)
If you're using PDO's prepared statements, you do not have to worry about the escaping yourself. No quotes, no backslashes, no nothing.