如果用户输入未经修改就插入到SQL查询中,则应用程序很容易受到SQL注入的攻击,如下例所示:

$unsafe_variable = $_POST['user_input']; 

mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");

这是因为用户可以输入类似值的内容);DROP TABLE表;--,并且查询变为:

INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')

可以采取什么措施防止这种情况发生?


当前回答

这个问题的简单替代方案可以通过在数据库本身中授予适当的权限来解决。例如:如果您使用的是MySQL数据库,则通过终端或提供的UI输入数据库,然后执行以下命令:

 GRANT SELECT, INSERT, DELETE ON database TO username@'localhost' IDENTIFIED BY 'password';

这将限制用户只能受限于指定的查询。删除删除权限,这样数据就永远不会从PHP页面发出的查询中删除。第二件事是刷新特权,以便MySQL刷新权限和更新。

FLUSH PRIVILEGES; 

有关刷新的详细信息。

要查看用户的当前权限,请启动以下查询。

select * from mysql.user where User='username';

了解更多有关GRANT的信息。

其他回答

一个好主意是使用像Idiorm这样的对象关系映射器:

$user = ORM::for_table('user')
->where_equal('username', 'j4mie')
->find_one();

$user->first_name = 'Jamie';
$user->save();

$tweets = ORM::for_table('tweet')
    ->select('tweet.*')
    ->join('user', array(
        'user.id', '=', 'tweet.user_id'
    ))
    ->where_equal('user.username', 'j4mie')
    ->find_many();

foreach ($tweets as $tweet) {
    echo $tweet->text;
}

它不仅可以避免SQL注入,还可以避免语法错误!它还支持具有方法链接的模型集合,以一次过滤或将操作应用于多个结果和多个连接。

安全警告:此答案不符合安全最佳实践。转义不足以防止SQL注入,请改用准备好的语句。

SQL语句中转义特殊字符的一些准则。

不要使用MySQL。此扩展已弃用。请改用MySQLi或PDO。

MySQLi

对于手动转义字符串中的特殊字符,可以使用mysqli_real_escape_string函数。除非使用mysqli_set_charset设置了正确的字符集,否则该函数将无法正常工作。

例子:

$mysqli = new mysqli('host', 'user', 'password', 'database');
$mysqli->set_charset('charset');

$string = $mysqli->real_escape_string($string);
$mysqli->query("INSERT INTO table (column) VALUES ('$string')");

要使用准备好的语句自动转义值,请使用mysqli_prepare和mysqli_stmt_bind_param,其中必须提供相应绑定变量的类型以进行适当的转换:

例子:

$stmt = $mysqli->prepare("INSERT INTO table (column1, column2) VALUES (?,?)");

$stmt->bind_param("is", $integer, $string);

$stmt->execute();

无论您使用prepared语句还是mysqli_real_escape_string,您都必须知道正在使用的输入数据的类型。

因此,如果使用准备好的语句,则必须指定mysqli_stmt_bind_param函数的变量类型。

正如名字所说,mysqli_real_escape_string的使用是为了转义字符串中的特殊字符,因此它不会使整数安全。此函数的目的是防止破坏SQL语句中的字符串,以及它可能对数据库造成的损坏。如果使用得当,mysqli_realescape_string是一个有用的函数,尤其是与sprintf结合使用时。

例子:

$string = "x' OR name LIKE '%John%";
$integer = '5 OR id != 0';

$query = sprintf( "SELECT id, email, pass, name FROM members WHERE email ='%s' AND id = %d", $mysqli->real_escape_string($string), $integer);

echo $query;
// SELECT id, email, pass, name FROM members WHERE email ='x\' OR name LIKE \'%John%' AND id = 5

$integer = '99999999999999999999';
$query = sprintf("SELECT id, email, pass, name FROM members WHERE email ='%s' AND id = %d", $mysqli->real_escape_string($string), $integer);

echo $query;
// SELECT id, email, pass, name FROM members WHERE email ='x\' OR name LIKE \'%John%' AND id = 2147483647

不推荐的警告:这个答案的示例代码(与问题的示例代码一样)使用了PHP的MySQL扩展,该扩展在PHP 5.5.0中被弃用,在PHP 7.0.0中被完全删除。安全警告:此答案不符合安全最佳实践。转义不足以防止SQL注入,请改用准备好的语句。使用以下概述的策略,风险自负。(此外,在PHP7中删除了mysql_real_ascape_string()。)

参数化查询AND输入验证是一种方法。尽管使用了mysql_real_ascape_string(),但在许多情况下可能会发生SQL注入。

这些示例易受SQL注入攻击:

$offset = isset($_GET['o']) ? $_GET['o'] : 0;
$offset = mysql_real_escape_string($offset);
RunQuery("SELECT userid, username FROM sql_injection_test LIMIT $offset, 10");

or

$order = isset($_GET['o']) ? $_GET['o'] : 'userid';
$order = mysql_real_escape_string($order);
RunQuery("SELECT userid, username FROM sql_injection_test ORDER BY `$order`");

在这两种情况下,都不能使用“”来保护封装。

来源:意外的SQL注入(当转义不够时)

对于那些不确定如何使用PDO(来自mysql_函数)的人,我制作了一个非常非常简单的PDO包装器,它是一个单独的文件。它的存在是为了显示应用程序需要做的所有常见事情是多么容易。适用于PostgreSQL、MySQL和SQLite。

基本上,在阅读手册的同时阅读它,了解如何在实际生活中使用PDO函数,从而使以所需格式存储和检索值变得简单。

我想要一列$count=数据库::列('SELECT count(*)FROM `user`');我想要一个数组(key=>value)结果(即,用于生成一个选择框)$pairs=数据库::pairs('SELECT `id',`username`FROM`user`');我想要单行结果$user=DB::行('SELECT*FROM`user`WHERE `id`=?',数组($user_id));我想要一系列结果$banned_users=DB::fetch('SELECT*FROM `user`WHERE `banned`=?',数组('RUE'));

我几年前就写过这个小函数:

function sqlvprintf($query, $args)
{
    global $DB_LINK;
    $ctr = 0;
    ensureConnection(); // Connect to database if not connected already.
    $values = array();
    foreach ($args as $value)
    {
        if (is_string($value))
        {
            $value = "'" . mysqli_real_escape_string($DB_LINK, $value) . "'";
        }
        else if (is_null($value))
        {
            $value = 'NULL';
        }
        else if (!is_int($value) && !is_float($value))
        {
            die('Only numeric, string, array and NULL arguments allowed in a query. Argument '.($ctr+1).' is not a basic type, it\'s type is '. gettype($value). '.');
        }
        $values[] = $value;
        $ctr++;
    }
    $query = preg_replace_callback(
        '/{(\\d+)}/', 
        function($match) use ($values)
        {
            if (isset($values[$match[1]]))
            {
                return $values[$match[1]];
            }
            else
            {
                return $match[0];
            }
        },
        $query
    );
    return $query;
}

function runEscapedQuery($preparedQuery /*, ...*/)
{
    $params = array_slice(func_get_args(), 1);
    $results = runQuery(sqlvprintf($preparedQuery, $params)); // Run query and fetch results.   
    return $results;
}

这允许在一行C#字符串中运行语句。格式如下:

runEscapedQuery("INSERT INTO Whatever (id, foo, bar) VALUES ({0}, {1}, {2})", $numericVar, $stringVar1, $stringVar2);

考虑到变量类型,它会逃逸。若您试图参数化表、列名称,它将失败,因为它将每个字符串放在引号中,这是无效语法。

安全更新:以前的str_replace版本允许通过向用户数据中添加{#}标记进行注入。如果替换包含这些令牌,则preg_replace_callback版本不会导致问题。