如果用户输入未经修改就插入到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;--')

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


当前回答

我认为如果有人想使用PHP和MySQL或其他数据库服务器:

考虑一下学习PDO(PHP数据对象)——它是一个数据库访问层,提供了访问多个数据库的统一方法。考虑学习MySQLi


库示例:

----个人数字助理

-----没有占位符-SQL注入时机成熟!这很糟糕

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) values ($name, $addr, $city)");

-----未命名占位符

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) values (?, ?, ?);

-----命名占位符

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) value (:name, :addr, :city)");

---MySQLi

$request = $mysqliConnection->prepare('
       SELECT * FROM trainers
       WHERE name = ?
       AND email = ?
       AND last_login > ?');

    $query->bind_param('first_param', 'second_param', $mail, time() - 3600);
    $query->execute();

P.S:

PDO轻松赢得了这场战斗。支持12个不同的数据库驱动程序和命名参数,我们可以习惯它的API。从安全的角度来看,只要开发人员按照应该使用的方式使用它们,它们都是安全的

其他回答

我认为,在PHP应用程序(或任何web应用程序)中防止SQL注入的最佳方法是考虑应用程序的架构。如果防止SQL注入的唯一方法是记住使用一个特殊的方法或函数,它在每次与数据库对话时都会做正确的事情,那么这是错误的。这样,在代码的某个时刻忘记正确格式化查询只是时间问题。

采用MVC模式和CakePHP或CodeIgniter这样的框架可能是正确的方法:创建安全数据库查询等常见任务已在这些框架中得到解决并集中实现。它们可以帮助您以合理的方式组织web应用程序,并让您更多地考虑加载和保存对象,而不是安全地构造单个SQL查询。

一个好主意是使用像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注入,还可以避免语法错误!它还支持具有方法链接的模型集合,以一次过滤或将操作应用于多个结果和多个连接。

要使用参数化查询,需要使用Mysqli或PDO。要用mysqli重写示例,我们需要以下内容。

<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli("server", "username", "password", "database_name");

$variable = $_POST["user-input"];
$stmt = $mysqli->prepare("INSERT INTO table (column) VALUES (?)");
// "s" means the database expects a string
$stmt->bind_param("s", $variable);
$stmt->execute();

你想在那里读到的关键函数是mysqli::prepare。

此外,正如其他人所建议的,您可能会发现使用PDO之类的东西来提升抽象层是有用的/更容易的。

请注意,您询问的案例相当简单,更复杂的案例可能需要更复杂的方法。特别地:

如果您希望根据用户输入更改SQL的结构,参数化查询将不会有帮助,并且mysql_real_ascape_string不包含所需的转义。在这种情况下,最好通过白名单传递用户的输入,以确保只允许通过“安全”值。

正如你所看到的,人们建议你最多使用准备好的陈述。这并没有错,但如果每个进程只执行一次查询,则会有轻微的性能损失。

我正面临这个问题,但我认为我以非常复杂的方式解决了它——黑客避免使用引号的方式。我将其与模拟的准备语句结合使用。我使用它来防止各种可能的SQL注入攻击。

我的方法:

如果您希望输入是整数,请确保它真的是整数。在像PHP这样的变量类型语言中,这一点非常重要。例如,您可以使用这个非常简单但功能强大的解决方案:sprintf(“SELECT 1,2,3 FROM table WHERE 4=%u”,$input);如果你对整数十六进制有任何期望,如果你对它十六进制,你将完全逃避所有输入。在C/C++中有一个名为mysql_hex_string()的函数,在PHP中可以使用bin2hex()。不用担心转义字符串的大小是其原始长度的2倍,因为即使使用mysql_real_ascape_string,PHP也必须分配相同的容量((2*input_length)+1),这是相同的。这种十六进制方法通常在传输二进制数据时使用,但我认为没有理由不对所有数据使用它来防止SQL注入攻击。请注意,您必须在数据前面加上0x,或者改用MySQL函数UNHEX。

例如,查询:

SELECT password FROM users WHERE name = 'root';

将变成:

SELECT password FROM users WHERE name = 0x726f6f74;

or

SELECT password FROM users WHERE name = UNHEX('726f6f74');

Hex是完美的逃脱。无法注射。

UNHEX函数与0x前缀之间的差异

评论中有一些讨论,所以我最后想说清楚。这两种方法非常相似,但在某些方面略有不同:

0x前缀只能用于char、varchar、text、block、binary等数据列。此外,如果要插入空字符串,则其使用有点复杂。您必须将其完全替换为“”,否则会出现错误。

UNHEX()适用于任何列;你不必担心空字符串。


十六进制方法通常用作攻击

注意,这种十六进制方法通常被用作SQL注入攻击,其中整数就像字符串一样,并且只使用mysql_real_aescape_string进行转义。然后可以避免使用引号。

例如,如果你只是这样做:

"SELECT title FROM article WHERE id = " . mysql_real_escape_string($_GET["id"])

攻击很容易给你注射。考虑从脚本返回的以下注入代码:

SELECT ... WHERE id = -1 UNION ALL SELECT table_name FROM information_schema.tables;

现在只提取表结构:

SELECT ... WHERE id = -1 UNION ALL SELECT column_name FROM information_schema.column WHERE table_name = __0x61727469636c65__;

然后只需选择所需的数据。是不是很酷?

但如果可注入站点的编码器将其十六进制,则不可能进行注入,因为查询将如下所示:

SELECT ... WHERE id = UNHEX('2d312075...3635');

如果可能,请转换参数的类型。但它只适用于像int、bool和float这样的简单类型。

$unsafe_variable = $_POST['user_id'];

$safe_variable = (int)$unsafe_variable ;

mysqli_query($conn, "INSERT INTO table (column) VALUES ('" . $safe_variable . "')");