In example 1402a if someone managed to pass
$_POST['user_id'] = " '') -- insert malicious sql here "
(that's quote, apostrophe, apostrophe, quote)
then wouldn't you be in trouble?
I always try to mysql_real_escape_string all values. Even when you expect them to be digits.
mysql_real_escape_string
(PHP 4 >= 4.3.0, PHP 5, PECL mysql:1.0)
mysql_real_escape_string — Escapes special characters in a string for use in a SQL statement
Description
string mysql_real_escape_string ( string $unescaped_string [, resource $link_identifier] )Escapes special characters in the unescaped_string, taking into account the current character set of the connection so that it is safe to place it in a mysql_query(). If binary data is to be inserted, this function must be used.
mysql_real_escape_string() calls MySQL's library function mysql_real_escape_string, which prepends backslashes to the following characters: \x00, \n, \r, \, ', " and \x1a.
This function must always (with few exceptions) be used to make data safe before sending a query to MySQL.
Parameters
- unescaped_string
The string that is to be escaped.
- link_identifier
The MySQL connection. If the link identifier is not specified, the last link opened by mysql_connect() is assumed. If no such link is found, it will try to create one as if mysql_connect() was called with no arguments. If by chance no connection is found or established, an E_WARNING level warning is generated.
Return Values
Returns the escaped string, or FALSE on error.
Examples
Example 1414. Simple mysql_real_escape_string() example
<?php
// Connect
$link = mysql_connect('mysql_host', 'mysql_user', 'mysql_password')
OR die(mysql_error());
// Query
$query = sprintf("SELECT * FROM users WHERE user='%s' AND password='%s'",
mysql_real_escape_string($user),
mysql_real_escape_string($password));
?>
Example 1415. An example SQL Injection Attack
<?php
// Query database to check if there are any matching users
$query = "SELECT * FROM users WHERE user='{$_POST['username']}' AND password='{$_POST['password']}'";
mysql_query($query);
// We didn't check $_POST['password'], it could be anything the user wanted! For example:
$_POST['username'] = 'aidan';
$_POST['password'] = "' OR ''='";
// This means the query sent to MySQL would be:
echo $query;
?>
The query sent to MySQL:
SELECT * FROM users WHERE user='aidan' AND password='' OR ''=''
This would allow anyone to log in without a valid password.
Example 1416. A "Best Practice" query
Using mysql_real_escape_string() around each variable prevents SQL Injection. This example demonstrates the "best practice" method for querying a database, independent of the Magic Quotes setting.
<?php
if (isset($_POST['product_name']) && isset($_POST['product_description']) && isset($_POST['user_id'])) {
// Connect
$link = mysql_connect('mysql_host', 'mysql_user', 'mysql_password');
if(!is_resource($link)) {
echo "Failed to connect to the server\n";
// ... log the error properly
} else {
// Reverse magic_quotes_gpc/magic_quotes_sybase effects on those vars if ON.
if(get_magic_quotes_gpc()) {
if(ini_get('magic_quotes_sybase')) {
$product_name = str_replace("''", "'", $_POST['product_name']);
$product_description = str_replace("''", "'", $_POST['product_description']);
} else {
$product_name = stripslashes($_POST['product_name']);
$product_description = stripslashes($_POST['product_description']);
}
} else {
$product_name = $_POST['product_name'];
$product_description = $_POST['product_description'];
}
// Make a safe query
$query = sprintf("INSERT INTO products (`name`, `description`, `user_id`) VALUES ('%s', '%s', %d)",
mysql_real_escape_string($product_name, $link),
mysql_real_escape_string($product_description, $link),
$_POST['user_id']);
mysql_query($query, $link);
if (mysql_affected_rows($link) > 0) {
echo "Product inserted\n";
}
}
} else {
echo "Fill the form properly\n";
}
?>
The query will now execute correctly, and SQL Injection attacks will not work.
Notes
Note: A MySQL connection is required before using mysql_real_escape_string() otherwise an error of level E_WARNING is generated, and FALSE is returned. If link_identifier isn't defined, the last MySQL connection is used.
Note: If magic_quotes_gpc is enabled, first apply stripslashes() to the data. Using this function on data which has already been escaped will escape the data twice.
Note: If this function is not used to escape data, the query is vulnerable to SQL Injection Attacks.
Note: mysql_real_escape_string() does not escape % and _. These are wildcards in MySQL if combined with LIKE, GRANT, or REVOKE.
See Also
| mysql_client_encoding() |
| addslashes() |
| stripslashes() |
| The magic_quotes_gpc directive |
| The magic_quotes_runtime directive |
mysql_real_escape_string
01-Jun-2007 03:04
17-May-2007 03:22
The function quote_smart provided will fail on the following kind of string
2E345668
The is_numeric will return true and will treat it as number. When it's passed to mysql, mysql'll think that it's a double and will try to parse it. But this number is much larger than the maximum allowed double in mysql. Thus, mysql will complain.
The fix is to add is_finite check when checking if the argument is a number.
06-May-2007 10:09
Quick update to example 1402.A, in the example no database is selected, and therefore the query won't work right. Here is a updated version with the database selected. If anyone has any thoughts on how to improve my selection method, please let me know.
<?php
if (isset($_POST['product_name']) && isset($_POST['product_description']) && isset($_POST['user_id'])) {
// Connect
$link = mysql_connect('mysql_host', 'mysql_user', 'mysql_password');
$select = mysql_select_db('mysql_name', $link);
if(!is_resource($link)) {
echo "Failed to connect to the server";
} elseif(!$select) {
echo "Failed to select database";
} else {
// Reverse magic_quotes_gpc/magic_quotes_sybase effects on those vars if ON.
if(get_magic_quotes_gpc()) {
if(ini_get('magic_quotes_sybase')) {
$product_name = str_replace("''", "'", $_POST['product_name']);
$product_description = str_replace("''", "'", $_POST['product_description']);
} else {
$product_name = stripslashes($_POST['product_name']);
$product_description = stripslashes($_POST['product_description']);
}
} else {
$product_name = $_POST['product_name'];
$product_description = $_POST['product_description'];
}
// Make a safe query
$query = sprintf("INSERT INTO products (`name`, `description`, `user_id`) VALUES ('%s', '%s', %d)",
mysql_real_escape_string($product_name, $link),
mysql_real_escape_string($product_description, $link),
$_POST['user_id']);
mysql_query($query, $link);
if (mysql_affected_rows($link) > 0) {
echo "Product inserted\n";
}
}
} else {
echo "Fill the form properly\n";
}
?>
Enjoy!
21-Nov-2006 06:44
Of course, all of this is moot if you use the new (pdp 5.1) PDO class and PDO::prepare($sql) with bound variables that protects against sql injection.
16-Nov-2006 05:35
The quote_smart function in the example above has no issues with losing leading zeros.
If the variable passed to it is numeric in nature then the variable is returned unchanged. This is fine as a number cannot be the source of an insertion attack.
If the variable passed to it is a numbic string (that is to say a string whose format is that of a number per the rules of is_numeric) then the variable is is also returned UNCHANGED. This is also fine because even though the variable is a string, we've confirmed that it contains only numbers, with a possible '.', '+', '-', 'e' (for exponential notation), or 'x' if it's a number in hex. None of these characters is a risk.
If the variable is a string and does not appear to be a number it is properly escaped.
20-Oct-2006 09:40
I concur with icydee. It's important that SQL Injection is prevented on the server side. A person (or software) doing SQL Injection on your site will often post to your page from one of his own making. For example, you may present a page like the following on http://examplesite.com/ that sanitizes all data before submitting.
<html>
<head><script type="text/javascript" src="validation.js" /></head>
<body>
<form method="POST" action="/login.php">
Name: <input type="text" name="the_acount" value="" /><br/>
Password: <input type="password" name="pwd" value="" /><br/>
<input type="submit" name="action" value="Submit" onClick="Sanitize()"/>
</body>
</html>
Your attacker views the source code and makes his own login page on his local hard drive. It doesn't have to look like or even load from your page as long as he posts the form data to your site using your expected parameters.
<html><body>
<form method="POST" action="http://examplesite.com/login.php">
<input type="hidden" name="the_acount" value="1'; drop table users; select 1 where name='" /><br/>
Password: <input type="hidden" name="pwd" value="Oh, was I supposed to be hashed?" /><br/>
<input type="submit" name="action" value="Submit" />
</body></html>
20-Oct-2006 02:06
I think instead of just using the quote_smart function to quote whatever the user gives you, you should ideally specify whether you are looking for a string or a number. For example if you have a user script and are using a sql statement like this, where username is a char or varchar field:
$sql="SELECT id FROM user_table WHERE user_name=".quote_smart($_POST['userName']);
if the user submits 12345 for example, you get:
SELECT id FROM user_table WHERE user_name=12345
Your DB 1) Must convert the 12345 to a string (not a huge deal) 2) Probably cannot use any index you have on that column (could be a bigger deal with a sizeable table)
So i think something more along the lines of:
function quote_smart($val,$valType) and handle the input based on $valType (being either string or numeric)
17-Oct-2006 12:26
After reading through all the different versions I came up with this:
function quote_smart($value)
{
if( is_array($value) ) {
return array_map("quote_smart", $value);
} else {
if( get_magic_quotes_gpc() ) {
$value = stripslashes($value);
}
if( $value == '' ) {
$value = 'NULL';
} if( !is_numeric($value) || $value[0] == '0' ) {
$value = "'".mysql_real_escape_string($value)."'";
}
return $value;
}
}
This version is a little slower but it will recursively handle arrays.
03-Oct-2006 03:37
To aide in the simplicity of things, I try and validate the information before it is even queried to the MySQL server.
For example. With usernames and passwords, I check for spaces. And ; signs. If either of these are in there, then the login request is ignored, since I do not allow either during registration.
So a simple attack like:
1 or 1=1
Isn't even sent. Instead a "Please check username / password and try again" message is displayed. I display the same message when the query is ignored as I do when an invalid login is parsed. As to not "challenge" the attacker. Make them think simply it was an invalide username/password, rather than a big deal like
die("Hacking attempt!!!");
You could very easily implement logging to keep track of malicious IP's and what-not as well.
Would this be better?
functions quote_smart($value) {
if(is_array($value)) {
if(get_magic_quotes_gpc()) {
$value=array_map("stripslashes",$value);
}
if(!array_map("is_number",$value)) {
$value=array_map("mysql_real_escape_string",$value);
}
}
else {
if(get_magic_quotes_gpc()) {
$value=stripslashes($value);
}
if(!is_number($value)) {
$value="'" . mysql_real_escape_string($value) . "'";
}
}
return $value;
}
06-Sep-2006 04:25
mysql_real_escape_string is a bit annoying when you need to do it over an array.
function mysql_real_escape_array($t){
return array_map("mysql_real_escape_string",$t);
}
this one just mysql_real_escape's the whole array.
ex) $_POST=mysql_real_escape_array($_POST);
and then you dont have to worry about forgetting to do this.
04-Sep-2006 01:31
To: eddypearson at gmail dot com
If thing that, this is better:
<?
$_POST = array_map('mysql_real_escape_string', $_POST);
?>
or
<?
$_SESSION = array_map('mysql_real_escape_string', $_SESSION);
?>
05-Aug-2006 11:36
<?php
include ('includes/db.php');
array_pop($_POST);
if ( get_magic_quotes_gpc() ) {
$_POST= array_map('stripslashes', $_POST);
}
$username= mysql_real_escape_string(trim($_POST['username']));
$password= mysql_real_escape_string(trim($_POST['password']));
$mdpwd= md5($password);
$sql= sprintf("SELECT COUNT(*) AS login_match FROM `users` WHERE `username` = '%s' AND `password`= '%s'", $username, $mdpwd);
$res= mysql_query($sql) or die(mysql_error());
$login_match= mysql_result($res, 0, 'login_match');
if ( $login_match == 1 ) {
//logged in
} else {
// not logged in
}
?>
28-Jul-2006 02:28
A quick function to mysql_real_escape_string every value in array (Think $_SESSION and $_POST guys).
This may be simple, and to some bloody obvious, but its useful little function, and I could find one on this page so:
function CleanArray($array) {
foreach ($array as $key => $value) {
$array[$key] = mysql_real_escape_string($value);
}
return $array;
}
20-Jul-2006 07:39
According to the newsforge refered to by Picky, it is adviseable to run PREPAREd statements in order to avoid injection. Plus, I created a table that stores pre-defined prepared statements and a procedure that runs that query. That way it's possible to control more tighly what's to be queried.
Example in mysql5 lingo:
create sys_queries (
name varchar(100), # query ID
mysql_statement text, #
primary key( name )
);
insert into sys_queries set name='search_users', 'SELECT * FROM users WHERE concat( username, name, email ) REGEXP ?';
delim $$
create sp_perform_query( _name varchar(100), _search_term varchar(255) )
begin
DECLARE _stat TEXT DEFAULT NULL;
SELECT mysql_statement INTO _stat FROM sys_queries WHERE name = _name;
IF _stat IS NOT NULL THEN
SET @a = _stat;
SET @b = _search_term;
PREPARE s FROM @a;
EXECUTE s USING @b;
END IF;
end;
$$
delim ;
in PHP5, use mysqli to call the procedure, first arg is the query name, second, the search arg. Still don't know a good way to pass multiple arguments to mysql, so please let me know.
18-Jul-2006 08:19
It seems to me that you could avoid many hassels by loading valid database values into an array at the beginning of the script, then instead of using user input to query the database directly, use it to query the array you've created. For example:
<?php
//you still have to query safely, so always use cleanup functions like eric256's
$categories = sql_query("select catName from categories where pageID = ?",$_GET['pageID']);
while ($cts = @mysql_fetch_row($categories)) {
//making $cts both the name and the value of the array variable makes it easier to check for in the future.
//obviously, this naming system wouldn't work for a multidimensional array
$cat_ar[$cts[0]] = $cts[0];
}
...
//user selects sorting criteria
//this would be from a query string like '?cats[]=cha&cats[]=fah&cats[]=lah&cats[]=badValue...', etc.
$cats = $_GET['cats'];
//verify that values exist in database before building sorting query
foreach($cats as $c) {
if ($cat_ar[$c]) { //instead of in_array(); maybe I'm just lazy... (see above note)
$cats1[] = "'".mysql_real_escape_string($c)."'";
}
}
$cats = $cats1;
//$cats now contains the filtered and escaped values of the query string
$cat_query = '&& (category_name = \''.implode(' || category_name = \'',$cats).'\')';
//build a sql query insert
//$cat_query is now "&& (category_name = 'cha' || category_name = 'fah' || category_name = 'lah')" - badValue has been removed
//since all values have already been verified and escaped, you can simply use them in a query
//however, since $pageID hasn't been cleaned for this query, you still have to use your cleaning function
$items = sql_query("SELECT * FROM items i, categories c WHERE i.catID = c.catID && pageID = ? $cat_query", $pageID);
30-May-2006 08:38
Note that mysql_real_escape_string doesn't prepend backslashes to \x00, \n, \r, and and \x1a as mentionned in the documentation, but actually replaces the character with a MySQL acceptable representation for queries (e.g. \n is replaced with the '\n' litteral). (\, ', and " are escaped as documented) This doesn't change how you should use this function, but I think it's good to know.
20-Apr-2006 09:52
eric256 at gmail dot com: the PEAR DB API provides exactly that. See http://pear.php.net/manual/en/package.database.db.intro-query.php
09-Apr-2006 03:27
Hey,
Since SQL injection is such a fear it is beyond me as to why this hasn't been included yet. I set this function up to take a query with ?'s in it and replace those with the correctly quoted values. I don't do the "smart quoteing" i quote it in the SQL because i like to see the quotes there.
<?php
function db_query($query) {
$args = func_get_args();
$query = array_shift($args);
$query = str_replace("?", "%s", $query);
$args = array_map('mysql_real_escape_string', $args);
array_unshift($args,$query);
$query = call_user_func_array('sprintf',$args);
$result = mysql_query($query) or die('Query failed: ' . mysql_error());
return $result;
}
?>
call it like
<?php
$results = db_query("SELECT * FROM users WHERE username='?' AND password = PASSWORD('?');", $username,$password);
?>
This automagicaly quotes them and executes the query for you and also dies if there is an error in the query.
14-Mar-2006 02:45
@ keith dot lawrence at jpmh dot co dot uk:
But an empty string is not per defenition NULL... usually you look for NULLable columns in your database on special occasions, so I think it's best to keep that apart...
13-Mar-2006 11:23
Here's my version, this replaces empty strings with NULLs instead of using a quoted empty string which was causing a sql error when adding empty values to an integer column which allows nulls. Your mileage may vary.
<?php
function quote_smart($value) {
if (get_magic_quotes_gpc()) $value = stripslashes($value);
if($value == '') $value = 'NULL';
else if (!is_numeric($value) || $value[0] == '0') $value = "'" . mysql_real_escape_string($value) . "'"; //Quote if not integer
return $value;
}
?>
12-Mar-2006 09:59
It's quite easy to stop the problem of 0666 returning as 666 in the example of quote_smart. Look at this:
<?php
// Quote variable to make safe
function quote_smart($value) {
// Stripslashes
if (get_magic_quotes_gpc()) {
$value = stripslashes($value);
}
// Quote if not integer
if (!is_numeric($value) || $value[0] == '0') {
$value = "'" . mysql_real_escape_string($value) . "'";
}
return $value;
}
?>
It now checks if there is a leading zero, and then it should be treated als a text, instead as a numerical value. Just an easy workaround.
28-Feb-2006 07:36
just tried on php 4.4.2 the quote_smart example works just fine with 0666 it comes back as 0666 and 00 comes back as 00. a single 0 gets lost however but I don't see that as a problem, unless of course you want that as your password. quote_smart works for me.
06-Feb-2006 04:20
as already said :
the example is wrong !
If your password is 0666 quote_smart will return : 666
Here's why pmioni is wrong:
http://shiflett.org/archive/184
13-Aug-2005 02:34
Note that to use quote_smart(), you have to connect to the database via mysql_connect(). Otherwise, mysql_real_escape_string() will have no way of knowing what database you want to use.
If you want to use the OO approach or mysqli_connect(), you're going to have to rewrite quote_smart() to accept a MySQL connection, which it would then pass to mysql_real_escape_string().
This is a pretty lackluster solution, though, for anyone concerned with abstraction.
07-Apr-2005 03:25
The problem with this function returning an empty string instead of an escaped string seems to be related to the mysql lib versions installed on the server. On 3 servers, each with php 4.3.10, I had no problems on 2 of them, but got the empty string on the 3rd. The 2 that worked had versions 4.1.x and 4.0.x of mysql. The 3rd that did not work had 3.23.x
The failure also only occurred when I did not have a mysql connection set up before running the escape function. If I did DB::connect (using the PEAR DB object) before running the function, it worked, even if I didn't pass in the connection id. If I ran the function before DB::connect, then I got an empty string.
So, if you are getting an empty string, check your mysql lib version and check where you are connecting to the db in relation to calling the escape function in your code.
11-Feb-2005 06:48
For a "best practices" approach to handling user input, one should always include enforcement of input length limitations. This will avoid potential attacks based on *very* large values being inserted, some of which may not be foiled just by escaping a string. (Length limits imposed via your form may be bypassed by submitting from a page or tool created by the attacker.) As a cursory example:
<?php
$maxNameLen = 25;
$limitedName = substr($_POST['username'],0,$maxNameLen);
$safeUsername = mysql_real_escape_string($limitedName);
?>
Note that you'll need to take care to truncate the correct (raw) value and not something that has already been processed. Otherwise, you're exposing the potential attack data to more potential points of failure, plus valid input may grow in length with escape processing and be incorrectly truncated.
21-Jan-2005 10:36
well, smth like that
<?php
function escape_string ($string,$dbcon=false) {
if(version_compare(phpversion(),"4.3.0")=="-1") {
mysql_escape_string($string);
} elseif ($dbcon) {
mysql_real_escape_string($string,$dbcon);
} else { return false; }
}
?>
15-Jan-2005 05:36
The quote_smart() function in the "Best Practice" section does not quote any value which is all numeric. This will drop all zero's from the the lefthand side of a string. While this is intended to simplify the query string when storing numeric values this has a negative effect when trying to store strings which just so happen to be all digits. For instance some zip codes have one or more zero's on the lefthand side, and an MD5 hash may contain all numeric characters and the lefthand characters can be zero's. In these cases the lefthand zero's will be dropped.
I would simply drop the is_numeric() check. There's no reason to not quote numeric values.
<?php
// bad query created using the quote_smart() function
$qs = "UPDATE tbl SET zipcode=" . quote_smart('01234');
// UPDATE tbl SET zipcode=1234
// acceptable query created after dropping the is_numeric() check
$qs = "UPDATE tbl SET number=" . quote_smart('01234');
// UPDATE tbl SET number='01234'
?>
01-Jan-2005 02:18
A case where you do not need to escape is when you are about to compare the UI (User Input) with a database through MD5 hashes, infact if you do, the password stored in the database will not match the one in the request.
I had a living hell trying to solve this in my earlier days, so I just wanted to enligthen any other newbies,
<?php
$try_pass=md5($_POST['u_pass']);
?>
is sufficient escaping.
Cheers
