如何在PHP中防止SQL注入?

2773
如果用户输入被直接插入到 SQL 查询中,那么应用程序就容易受到 SQL 注入攻击,例如以下示例:

If user input is inserted without modification into an SQL query, then the application becomes vulnerable to SQL injection, like in the following example:

$unsafe_variable = $_POST['user_input']; 

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

这是因为用户可以输入类似于value'); DROP TABLE table;--这样的内容,然后查询就会变成:

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

有什么方法可以防止这种情况发生吗?

27个回答

180
安全警告: 本回答未遵循最佳安全实践。 转义不足以防止SQL注入, 应使用预处理语句(prepared statements)。请自行承担以下策略的风险。(此外,mysql_real_escape_string()在PHP 7中已被移除。) 弃用警告: mysql扩展目前已被弃用。我们建议使用PDO扩展

我有三种不同的方法来防止我的Web应用程序易受SQL注入攻击。

  1. 使用mysql_real_escape_string(),这是在PHP中预定义的函数,此代码会在以下字符添加反斜杠:\x00\n\r\'"\x1a。将输入值作为参数传递以最小化SQL注入的可能性。
  2. 最先进的方法是使用PDOs。

希望这能帮到您。

考虑以下查询:

$iId = mysql_real_escape_string("1 OR 1=1"); $sSql = "SELECT * FROM table WHERE id = $iId";

mysql_real_escape_string() 在这里无法保护。如果在查询内部的变量周围使用单引号(' ')可以保护您免受此类攻击。以下是解决方案:

$iId = (int) mysql_real_escape_string("1 OR 1=1"); $sSql = "SELECT * FROM table WHERE id = $iId";

这个question有一些关于此的好回答。

我建议使用PDO是最好的选择。

编辑:

mysql_real_escape_string()自PHP 5.5.0起已被弃用。请使用mysqli或PDO。

mysql_real_escape_string()的替代方法是

string mysqli_real_escape_string ( mysqli $link , string $escapestr )

例子:

$iId = $mysqli->real_escape_string("1 OR 1=1");
$mysqli->query("SELECT * FROM table WHERE id = $iId");

171

一个简单的方法是使用PHP框架,例如CodeIgniterLaravel,它们具有内置的过滤和活动记录功能,因此您不必担心这些细节。


11
我认为这个问题的要点在于不使用这样的框架来完成。 - Sanke

152

警告:此答案中描述的方法仅适用于非常特定的场景,并且不安全,因为SQL注入攻击不仅仅依赖于能够注入X=Y

如果攻击者试图通过PHP的$_GET变量或URL的查询字符串来黑客进入表单,如果他们没有保护,则您将能够捕获他们。

RewriteCond %{QUERY_STRING} ([0-9]+)=([0-9]+)
RewriteRule ^(.*) ^/track.php
因为 1=12=21=22=11+1=2 等常见问题是攻击者针对 SQL 数据库提出的。也许许多黑客应用程序也在使用。
但你必须小心,不要重写来自您网站的安全查询。上面的代码给出了一个提示,将针对黑客的动态查询字符串重写或重定向 (这取决于您) 到一个页面,该页面将存储攻击者的IP地址,甚至是他们的 Cookies、历史记录、浏览器或任何其他敏感信息,以便您稍后通过禁止他们的帐户或与当局联系来处理它们。

136

一个好的想法是使用对象关系映射器,例如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 注入,还可以避免语法错误!它还支持使用方法链接集合中的模型,以便一次性地对多个结果进行过滤或应用操作,并支持多个连接。


我对你的建议持不同意见。这可能会导致错误的安全感,将任何ORM都引入其中。当然,大多数ORM都会处理准备好的语句和参数化查询。来到这篇文章的新手可能仍然会通过选择任何ORM来感到安全 - 相信它们全部。总的来说,ORM通过隐藏/抽象实现细节来简化事情。你真的想要检查(或盲目相信)它是如何完成的。经验法则:开源社区(支持)越大,它就越不会完全失败 ;) - pocketrocket
1
说实话,这不是最糟糕的想法,PocketRocket。根据ORM的不同,ORM的开发者很有可能比编码人员更了解SQL。这有点像加密学中的那个古老规则:除非你在该领域的研究论文上署名,否则不要自己动手设计加密算法,因为攻击者很可能已经在该领域的论文上署名了。话虽如此,如果ORM要求您提供查询的全部或部分内容(例如Model.filter('where foo = ?',bar)),那么你很可能最好还是自己写手写SQL。 - Shayne

131

有很多关于PHP和MySQL的答案,但是这里提供用于PHP和Oracle的代码,既可以防止SQL注入,也可以正常使用oci8驱动程序:

$conn = oci_connect($username, $password, $connection_string);
$stmt = oci_parse($conn, 'UPDATE table SET field = :xx WHERE ID = 123');
oci_bind_by_name($stmt, ':xx', $fieldval);
oci_execute($stmt);

126
警告:已弃用 此答案的示例代码(与问题的示例代码相同)使用了 PHP 的 MySQL 扩展,该扩展在 PHP 5.5.0 中已被弃用,并在 PHP 7.0.0 中完全移除。 安全警告:此答案不符合安全最佳实践。转义不足以防止 SQL 注入,请改用预处理语句。请自行承担以下所述策略的风险。(此外,mysql_real_escape_string() 已在 PHP 7 中移除。)
使用 PDOMYSQLi 是防止 SQL 注入的良好实践,但如果您确实想使用 MySQL 函数和查询,最好使用 mysql_real_escape_string
$unsafe_variable = mysql_real_escape_string($_POST['user_input']);

有更多的能力来防止这种情况发生:比如识别 - 如果输入是字符串、数字、字符或数组,有很多内置函数可以检测这些。此外,最好使用这些函数来检查输入数据。 is_string
$unsafe_variable = (is_string($_POST['user_input']) ? $_POST['user_input'] : '');

is_numeric

$unsafe_variable = (is_numeric($_POST['user_input']) ? $_POST['user_input'] : '');

使用这些函数来检查输入数据与mysql_real_escape_string一起使用会更好。

10
同样地,使用is_string()检查$_POST数组中的成员没有任何意义。 - Your Common Sense
21
警告!mysql_real_escape_string()不是万无一失的。参考链接:https://dev59.com/Ym025IYBdhLWcg3w4qEx。 - eggyal
10
mysql_real_escape_string已经被弃用,因此不再是可行的选项。它将在未来从PHP中删除。最好转向PHP或MySQL官方推荐的替代方案。 - jww

90

几年前我写了这个小函数:

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#的String.Format字符串中运行语句,例如:

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

它在考虑变量类型时会逃避。如果您尝试将表格、列名参数化,由于它将每个字符串放在引号中,这是无效的语法,因此会失败。

安全更新:之前的str_replace版本允许通过将{#}令牌添加到用户数据中进行注入。这个preg_replace_callback版本如果替换内容包含这些令牌不会出现问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接