2773

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

$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;--')

可以做些什么来防止这种情况发生?

4

28 回答 28

9408

无论您使用哪个数据库,避免 SQL 注入攻击的正确方法是将数据与 SQL 分离,以便数据保持数据,永远不会被 SQL 解析器解释为命令。可以使用格式正确的数据部分创建 SQL 语句,但如果您不完全了解详细信息,则应始终使用准备好的语句和参数化查询。这些是与任何参数分开发送到数据库服务器并由其解析的 SQL 语句。这样攻击者就不可能注入恶意 SQL。

你基本上有两种选择来实现这一点:

  1. 使用PDO(对于任何支持的数据库驱动程序):

     $stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
    
     $stmt->execute([ 'name' => $name ]);
    
     foreach ($stmt as $row) {
         // Do something with $row
     }
    
  2. 使用MySQLi(用于 MySQL):

     $stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
     $stmt->bind_param('s', $name); // 's' specifies the variable type => 'string'
    
     $stmt->execute();
    
     $result = $stmt->get_result();
     while ($row = $result->fetch_assoc()) {
         // Do something with $row
     }
    

如果您要连接到 MySQL 以外的数据库,则可以参考特定于驱动程序的第二个选项(例如,pg_prepare()对于pg_execute()PostgreSQL)。PDO 是通用选项。


正确设置连接

请注意,当使用PDO访问 MySQL 数据库时,默认情况下不使用实际准备好的语句。要解决此问题,您必须禁用准备好的语句的模拟。使用PDO创建连接的示例是:

$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'password');

$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

在上面的示例中,错误模式并不是绝对必要的,但建议添加它。这样脚本就不会Fatal Error在出现问题时停止。它使开发人员有机会处理n as s 的catch任何错误。throwPDOException

然而,强制性的是第一setAttribute()行,它告诉 PDO 禁用模拟的预准备语句并使用真正的预准备语句。这确保了语句和值在将其发送到 MySQL 服务器之前不会被 PHP 解析(使可能的攻击者没有机会注入恶意 SQL)。

尽管您可以charset在构造函数的选项中设置 ,但重要的是要注意“旧”版本的 PHP(5.3.6 之前)会默默地忽略DSN 中的 charset 参数。


解释

您传递给的 SQL 语句prepare由数据库服务器解析和编译。通过指定参数(如上例中的?参数或命名参数:name),您可以告诉数据库引擎您要过滤的位置。然后,当您调用 时execute,准备好的语句将与您指定的参数值相结合。

这里重要的是参数值与编译语句组合在一起,而不是 SQL 字符串。SQL 注入的工作原理是在创建要发送到数据库的 SQL 时诱使脚本包含恶意字符串。因此,通过将实际 SQL 与参数分开发送,您可以限制以您不想要的东西结束的风险。

您在使用准备好的语句时发送的任何参数都将被视为字符串(尽管数据库引擎可能会进行一些优化,因此参数也可能最终以数字形式结束)。在上面的示例中,如果$name变量包含'Sarah'; DELETE FROM employees结果将只是对 string 的搜索"'Sarah'; DELETE FROM employees",并且您最终不会得到一个空 table

使用准备好的语句的另一个好处是,如果您在同一个会话中多次执行相同的语句,它只会被解析和编译一次,从而为您带来一些速度提升。

哦,既然你问过如何插入,这里有一个例子(使用 PDO):

$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');

$preparedStatement->execute([ 'column' => $unsafeValue ]);

准备好的语句可以用于动态查询吗?

虽然您仍然可以为查询参数使用准备好的语句,但动态查询本身的结构无法参数化,某些查询功能也无法参数化。

对于这些特定场景,最好的办法是使用限制可能值的白名单过滤器。

// Value whitelist
// $dir can only be 'DESC', otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
   $dir = 'ASC';
}
于 2008-09-13T12:30:26.950 回答
1711

不推荐使用的警告: 此答案的示例代码(如问题的示例代码)使用 PHP 的MySQL扩展,该扩展在 PHP 5.5.0 中已弃用,并在 PHP 7.0.0 中完全删除。

安全警告:此答案不符合安全最佳实践。转义不足以防止 SQL 注入,请改用准备好的语句。使用下面概述的策略,风险自负。(另外,mysql_real_escape_string()在 PHP 7 中被删除。)

如果您使用的是最新版本的 PHP,mysql_real_escape_string下面列出的选项将不再可用(尽管mysqli::escape_string是现代的等效选项)。如今,该mysql_real_escape_string选项仅对旧版本 PHP 上的遗留代码有意义。


您有两个选择 - 转义 中的特殊字符unsafe_variable,或使用参数化查询。两者都可以保护您免受 SQL 注入。参数化查询被认为是更好的做法,但需要在 PHP 中更改为较新的 MySQL 扩展,然后才能使用它。

我们将首先介绍较低的影响字符串逃脱一个。

//Connect

$unsafe_variable = $_POST["user-input"];
$safe_variable = mysql_real_escape_string($unsafe_variable);

mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

//Disconnect

另请参阅mysql_real_escape_string函数的详细信息。

要使用参数化查询,您需要使用MySQLi而不是MySQL函数。要重写您的示例,我们需要以下内容。

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

    // TODO - Check that connection was successful.

    $unsafe_variable = $_POST["user-input"];

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

    // TODO check that $stmt creation succeeded

    // "s" means the database expects a string
    $stmt->bind_param("s", $unsafe_variable);

    $stmt->execute();

    $stmt->close();

    $mysqli->close();
?>

您要阅读的关键功能是mysqli::prepare.

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

请注意,您询问的案例是一个相当简单的案例,更复杂的案例可能需要更复杂的方法。尤其:

  • 如果您想根据用户输入更改 SQL 的结构,参数化查询将无济于事,并且所需的转义不在mysql_real_escape_string. 在这种情况下,您最好将用户的输入通过白名单传递,以确保只允许“安全”值通过。
  • 如果您在条件中使用来自用户输入的整数并采用该mysql_real_escape_string方法,您将遇到下面评论中多项式描述的问题。这种情况比较棘手,因为整数不会被引号包围,因此您可以通过验证用户输入仅包含数字来处理。
  • 可能还有其他我不知道的情况。您可能会发现是一个有用的资源,可以解决您可能遇到的一些更微妙的问题。
于 2008-09-13T09:48:48.617 回答
1129

这里的每个答案都只涵盖了问题的一部分。事实上,我们可以动态地将四个不同的查询部分添加到 SQL 中:-

  • 一个字符串
  • 一个号码
  • 标识符
  • 语法关键字

准备好的陈述只涵盖其中两个。

但有时我们必须使查询更加动态,同时添加运算符或标识符。因此,我们将需要不同的保护技术。

一般来说,这种保护方法是基于白名单的。

在这种情况下,每个动态参数都应该在您的脚本中进行硬编码并从该集合中选择。例如,要进行动态排序:

$orders  = array("name", "price", "qty"); // Field names
$key = array_search($_GET['sort'], $orders)); // if we have such a name
$orderby = $orders[$key]; // If not, first one will be set automatically. 
$query = "SELECT * FROM `table` ORDER BY $orderby"; // Value is safe

为了简化这个过程,我编写了一个白名单辅助函数,它可以在一行中完成所有工作:

$orderby = white_list($_GET['orderby'], "name", ["name","price","qty"], "Invalid field name");
$query  = "SELECT * FROM `table` ORDER BY `$orderby`"; // sound and safe

还有另一种保护标识符的方法 - 转义,但我宁愿坚持将白名单作为一种更强大和明确的方法。然而,只要您引用了标识符,您就可以转义引号字符以使其安全。例如,默认情况下,对于 mysql,您必须将引号字符加倍才能对其进行转义。对于其他其他 DBMS 转义规则会有所不同。

尽管如此,SQL 语法关键字(例如AND,DESC等)仍然存在问题,但在这种情况下,白名单似乎是唯一的方法。

因此,一般建议可以表述为

  • 任何表示 SQL 数据文字的变量(或者,简单地说 - SQL 字符串或数字)都必须通过准备好的语句添加。没有例外。
  • 任何其他查询部分,例如 SQL 关键字、表或字段名称或运算符 - 必须通过白名单进行过滤。

更新

尽管关于 SQL 注入保护的最佳实践达成了普遍共识,但仍然存在许多不好的实践。其中一些在 PHP 用户的脑海中根深蒂固。例如,在这个页面上(尽管大多数访问者看不到)超过 80 个已删除的答案- 由于质量差或宣传不良和过时的做法,所有答案都被社区删除。更糟糕的是,一些不好的答案并没有被删除,而是蓬勃发展。

例如,there(1) are(2) still(3) many(4) answers(5),包括建议您手动转义字符串的第二个最受好评的答案- 一种已被证明不安全的过时方法。

或者有一个稍微好一点的答案,它暗示了另一种字符串格式化方法,甚至把它吹捧为终极灵丹妙药。当然,事实并非如此。这种方法并不比常规字符串格式化好,但它保留了所有缺点:它仅适用于字符串,并且与任何其他手动格式化一样,它本质上是可选的、非强制性的措施,容易出现任何类型的人为错误。

我认为这一切都是因为一个非常古老的迷信,得到了OWASPPHP 手册等权威机构的支持,它宣称任何“转义”和 SQL 注入保护之间都是平等的。

不管 PHP 手册多年来说什么,*_escape_string绝不会保证数据安全,也从来没有打算这样做。除了对字符串以外的任何 SQL 部分无用之外,手动转义也是错误的,因为它是手动的,与自动相反。

OWASP 让情况变得更糟,强调转义用户输入,这完全是胡说八道:在注入保护的上下文中不应该有这样的词。每个变量都有潜在的危险 - 无论来源如何!或者,换句话说 - 每个变量都必须正确格式化才能放入查询中 - 无论来源如何。重要的是目的地。开发人员开始将绵羊与山羊分开的那一刻(考虑某个特定变量是否“安全”)他/她迈出了走向灾难的第一步。更不用说,甚至措辞都暗示在入口点进行批量转义,类似于非常神奇的引号功能 - 已经被鄙视、弃用和删除。

因此,与任何“转义”不同,准备好的语句确实防止 SQL 注入的措施(如果适用)。

于 2011-11-24T09:50:49.593 回答
882

我建议使用PDO(PHP 数据对象)来运行参数化 SQL 查询。

这不仅可以防止 SQL 注入,还可以加快查询速度。

通过使用 PDO 而不是mysql_mysqli_pgsql_函数,您可以使您的应用程序从数据库中更加抽象一点,在极少数情况下您必须切换数据库提供程序。

于 2008-09-13T00:02:20.430 回答
654

使用PDO和准备好的查询。

$conn是一个PDO对象)

$stmt = $conn->prepare("INSERT INTO tbl VALUES(:id, :name)");
$stmt->bindValue(':id', $id);
$stmt->bindValue(':name', $name);
$stmt->execute();
于 2008-09-13T13:20:56.860 回答
577

如您所见,人们建议您最多使用准备好的语句。这没有错,但是当您的查询在每个进程中只执行一次时,性能会受到轻微影响。

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

我的做法:

  • 如果您希望输入是整数,请确保它确实是整数。在像 PHP 这样的可变类型语言中,这一点非常重要。例如,您可以使用这个非常简单但功能强大的解决方案:sprintf("SELECT 1,2,3 FROM table WHERE 4 = %u", $input);

  • 如果您期望从整数十六进制中获得任何其他信息。如果你对它进行十六进制,你将完美地逃避所有输入。在 C/C++ 中有一个函数叫做mysql_hex_string(),在 PHP 中你可以使用bin2hex().

    不用担心转义字符串的大小会是其原始长度的 2 倍,因为即使使用mysql_real_escape_string,PHP 也必须分配相同的容量((2*input_length)+1),这是相同的。

  • 传输二进制数据时经常使用这种十六进制方法,但我认为没有理由不对所有数据使用它来防止 SQL 注入攻击。请注意,您必须在数据前添加0x或使用 MySQL 函数UNHEX

因此,例如,查询:

SELECT password FROM users WHERE name = 'root';

会变成:

SELECT password FROM users WHERE name = 0x726f6f74;

或者

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

Hex是完美的逃生方式。没办法注入。

UNHEX函数和0x前缀的区别

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

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

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


十六进制方法经常被用作攻击

请注意,这种十六进制方法通常用作 SQL 注入攻击,其中整数就像字符串一样,只是用mysql_real_escape_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');
于 2012-10-03T14:07:42.387 回答
513

不推荐使用的警告: 此答案的示例代码(如问题的示例代码)使用 PHP 的MySQL扩展,该扩展在 PHP 5.5.0 中已弃用,并在 PHP 7.0.0 中完全删除。

安全警告:此答案不符合安全最佳实践。转义不足以防止 SQL 注入,请改用准备好的语句。使用下面概述的策略,风险自负。(另外,mysql_real_escape_string()在 PHP 7 中被删除。)

重要的

正如公认的答案所示,防止 SQL 注入的最佳方法是使用Prepared Statements 而不是 escaping

Aura.SqlEasyDB等库允许开发人员更轻松地使用准备好的语句。要详细了解为什么准备好的语句更擅长阻止 SQL 注入,请参阅mysql_real_escape_string()绕过最近修复的 WordPress 中的 Unicode SQL 注入漏洞

注入预防 - mysql_real_escape_string()

PHP 有一个专门的函数来防止这些攻击。您需要做的就是使用满口的功能,mysql_real_escape_string.

mysql_real_escape_string接受一个将在 MySQL 查询中使用的字符串,并返回相同的字符串,所有 SQL 注入尝试都安全转义。基本上,它将用一个 MySQL 安全的替代品,一个转义的引号 \' 替换用户可能输入的那些麻烦的引号(')。

注意:您必须连接到数据库才能使用此功能!

// 连接到 MySQL

$name_bad = "' OR 1'"; 

$name_bad = mysql_real_escape_string($name_bad);

$query_bad = "SELECT * FROM customers WHERE username = '$name_bad'";
echo "Escaped Bad Injection: <br />" . $query_bad . "<br />";


$name_evil = "'; DELETE FROM customers WHERE 1 or username = '"; 

$name_evil = mysql_real_escape_string($name_evil);

$query_evil = "SELECT * FROM customers WHERE username = '$name_evil'";
echo "Escaped Evil Injection: <br />" . $query_evil;

您可以在MySQL-SQL 注入防护中找到更多详细信息。

于 2011-06-17T04:00:21.353 回答
480

你可以做一些基本的事情是这样的:

$safe_variable = mysqli_real_escape_string($_POST["user-input"], $dbConnection);
mysqli_query($dbConnection, "INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

这不会解决所有问题,但它是一个很好的垫脚石。我省略了明显的项目,例如检查变量的存在、格式(数字、字母等)。

于 2008-09-13T00:15:53.283 回答
393

无论您最终使用什么,请确保您检查您的输入是否已经被magic_quotes或其他一些善意的垃圾所破坏,如有必要,请运行它stripslashes或进行任何清理。

于 2008-12-08T05:26:35.927 回答
375

不推荐使用的警告: 此答案的示例代码(如问题的示例代码)使用 PHP 的MySQL扩展,该扩展在 PHP 5.5.0 中已弃用,并在 PHP 7.0.0 中完全删除。

安全警告:此答案不符合安全最佳实践。转义不足以防止 SQL 注入,请改用准备好的语句。使用下面概述的策略,风险自负。(另外,mysql_real_escape_string()在 PHP 7 中被删除。)

参数化查询和输入验证是要走的路。尽管mysql_real_escape_string()已经使用了 SQL 注入,但在许多情况下都可能发生 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");

或者

$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 注入(当转义不够时)

于 2011-07-03T21:50:15.280 回答
320

在我看来,通常在 PHP 应用程序(或任何 Web 应用程序)中防止 SQL 注入的最佳方法是考虑应用程序的架构。如果防止 SQL 注入的唯一方法是记住每次与数据库交谈时都使用一种特殊的方法或函数来执行正确的事情,那么你做错了。这样,您忘记在代码中的某个位置正确格式化查询只是时间问题。

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

于 2012-07-03T10:14:50.590 回答
311

从安全的角度来看,我喜欢存储过程MySQL 从 5.0 开始就支持存储过程)——优点是——

  1. 大多数数据库(包括MySQL)允许用户访问被限制为执行存储过程。细粒度的安全访问控制有助于防止权限升级攻击。这可以防止受感染的应用程序能够直接针对数据库运行 SQL。
  2. 它们从应用程序中抽象出原始 SQL 查询,因此应用程序可以使用的数据库结构信息较少。这使得人们更难理解数据库的底层结构和设计合适的攻击。
  3. 它们只接受参数,因此参数化查询的优势就在那里。当然 - IMO 你仍然需要清理你的输入 - 特别是如果你在存储过程中使用动态 SQL。

缺点是——

  1. 它们(存储过程)很难维护,并且往往会迅速增加。这使得管理它们成为一个问题。
  2. 它们不太适合动态查询——如果它们被构建为接受动态代码作为参数,那么很多优点都被否定了。
于 2009-11-03T18:05:20.810 回答
311

有很多方法可以防止 SQL 注入和其他 SQL 黑客攻击。您可以在 Internet 上轻松找到它(Google 搜索)。当然,PDO 是很好的解决方案之一。但我想建议你一些好的链接预防 SQL 注入。

什么是 SQL 注入以及如何预防

SQL注入的PHP手册

微软讲解PHP中SQL注入及预防

还有一些其他的,比如用 MySQL 和 PHP 防止 SQL 注入

现在,为什么你需要防止你的查询被 SQL 注入?

我想让您知道:为什么我们要尝试通过以下简短示例来防止 SQL 注入:

登录认证匹配查询:

$query="select * from users where email='".$_POST['email']."' and password='".$_POST['password']."' ";

现在,如果有人(黑客)把

$_POST['email']= admin@emali.com' OR '1=1

和密码什么的......

查询将被解析到系统中,直到:

$query="select * from users where email='admin@emali.com' OR '1=1';

另一部分将被丢弃。那么,会发生什么?未经授权的用户(黑客)无需密码即可以管理员身份登录。现在,他/她可以做管理员/电子邮件人员可以做的任何事情。看,如果不阻止 SQL 注入,那是非常危险的。

于 2012-07-23T10:19:50.530 回答
274

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

  1. 考虑学习PDO(PHP 数据对象)——它是一个数据库访问层,提供访问多个数据库的统一方法。
  2. 考虑学习MySQLi
  3. 使用原生 PHP 函数,例如:strip_tagsmysql_real_escape_string或如果变量为数字,则为(int)$foo. 在此处阅读有关 PHP 中变量类型的更多信息。如果您使用 PDO 或 MySQLi 等库,请始终使用PDO::quote()mysqli_real_escape_string()

库示例:

---- PDO

----- 没有占位符 - 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();

PS

PDO 轻松赢得了这场战斗。由于支持十二种不同的数据库驱动程序和命名参数,我们可以忽略小的性能损失,并习惯它的 API。从安全的角度来看,只要开发人员以应有的方式使用它们,它们都是安全的

但是,虽然 PDO 和 MySQLi 都非常快,但 MySQLi 在基准测试中的执行速度却微不足道——非准备语句约为 2.5%,准备语句约为 6.5%。

请测试对您数据库的每个查询 - 这是防止注入的更好方法。

于 2012-10-10T14:53:49.153 回答
267

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

$unsafe_variable = $_POST['user_id'];

$safe_variable = (int)$unsafe_variable ;

mysqli_query($conn, "INSERT INTO table (column) VALUES ('" . $safe_variable . "')");
于 2012-06-12T08:03:08.573 回答
238

如果您想利用缓存引擎,如RedisMemcached,也许 DALMP 可能是一个选择。它使用纯MySQLi。检查这个:使用 PHP 的 MySQL 的 DALMP 数据库抽象层。

此外,您可以在准备查询之前“准备”您的参数,以便您可以构建动态查询并最终获得完全准备好的语句查询。使用 PHP 的 MySQL 的 DALMP 数据库抽象层。

于 2012-10-15T13:52:37.080 回答
231

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

基本上,在阅读手册的同时阅读它,了解如何将 PDO 函数用于现实生活,以便以所需格式轻松存储和检索值。

我想要一个单列

$count = DB::column('SELECT COUNT(*) FROM `user`');

我想要一个数组(键 => 值)结果(即用于制作选择框)

$pairs = DB::pairs('SELECT `id`, `username` FROM `user`');

我想要一个单行结果

$user = DB::row('SELECT * FROM `user` WHERE `id` = ?', array($user_id));

我想要一系列结果

$banned_users = DB::fetch('SELECT * FROM `user` WHERE `banned` = ?', array('TRUE'));
于 2012-09-19T18:10:33.653 回答
227

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

不要使用MySQL。此扩展已弃用。请改用MySQLiPDO

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_preparemysqli_stmt_bind_param,其中必须提供相应绑定变量的类型以进行适当的转换:

例子:

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

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

$stmt->execute();

无论您使用预准备语句还是mysqli_real_escape_string,您始终必须知道您正在使用的输入数据的类型。

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

顾名思义,它的用途mysqli_real_escape_string是转义字符串中的特殊字符,因此它不会使整数变得安全。此函数的目的是防止破坏 SQL 语句中的字符串,以及它可能导致的数据库损坏。mysqli_real_escape_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
于 2013-05-09T16:36:55.423 回答
226

使用此 PHP 功能mysql_escape_string(),您可以快速获得良好的预防。

例如:

SELECT * FROM users WHERE name = '".mysql_escape_string($name_from_html_form)."'

mysql_escape_string— 转义字符串以在 mysql_query 中使用

为了进一步预防,您可以在末尾添加...

wHERE 1=1   or  LIMIT 1

最后你得到:

SELECT * FROM users WHERE name = '".mysql_escape_string($name_from_html_form)."' LIMIT 1
于 2012-08-03T19:59:31.307 回答
186

这个问题的简单替代方案可以通过在数据库本身中授予适当的权限来解决。例如:如果您使用的是 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的信息。

于 2012-10-25T08:07:25.437 回答
182

关于许多有用的答案,我希望为这个线程增加一些价值。

SQL 注入是一种可以通过用户输入(由用户填写然后在查询中使用的输入)来完成的攻击。SQL 注入模式是正确的查询语法,我们可以称之为:出于不良原因的不良查询,并且我们假设可能有一个坏人试图获取影响安全三原则(机密性)的秘密信息(绕过访问控制) 、完整性和可用性)。

现在,我们的重点是防止SQL注入攻击等安全威胁,问的问题(如何使用PHP防止SQL注入攻击),更现实一点,数据过滤或清除输入数据是在使用用户输入数据时的情况这样的查询,使用 PHP 或任何其他编程语言并非如此,或者更多人建议使用现代技术,例如准备好的语句或任何其他当前支持 SQL 注入预防的工具,是否认为这些工具不再可用?您如何保护您的应用程序?

我反对 SQL 注入的方法是:在将用户输入数据发送到数据库之前(在任何查询中使用它之前)清除用户输入数据。

数据过滤(将不安全数据转换为安全数据)

考虑到PDOMySQLi不可用。如何保护您的应用程序?你强迫我使用它们吗?PHP 以外的其他语言呢?我更喜欢提供一般性的想法,因为它可以用于更广泛的边界,而不仅仅是特定的语言。

  1. SQL用户(限制用户权限):最常见的SQL操作是(SELECT,UPDATE,INSERT),那么,为什么要给不需要的用户UPDATE权限呢?比如登录和搜索页面都只使用了SELECT,那么,为什么在这些页面中使用DB用户的权限很高呢?

规则:不要为所有权限创建一个数据库用户。对于所有 SQL 操作,您可以创建您的方案(如(deluser、selectuser、updateuser)作为用户名以便于使用。

参见最小特权原则

  1. 数据过滤:在构建任何查询用户输入之前,应该对其进行验证和过滤。对于程序员来说,为每个用户输入变量定义一些属性很重要: 数据类型、数据模式和数据长度。必须使用精确规则精确验证 (x 和 y) 之间的数字字段,对于字符串(文本)字段:模式就是这种情况,例如,用户名必须只包含一些字符,让我们说[a-zA-Z0-9_-.]。长度在 (x 和 n) 之间变化,其中 x 和 n (整数,x <=n)。 规则:创建精确的过滤器和验证规则对我来说是最佳实践。

  2. 使用其他工具:在这里,我也同意您的说法,即准备好的语句(参数化查询)和存储过程。这里的缺点是这些方法需要大多数用户不具备的高级技能。这里的基本思想是区分 SQL 查询和内部使用的数据。即使是不安全的数据也可以使用这两种方法,因为这里的用户输入数据不会向原始查询添加任何内容,例如 (any 或 x=x)。

有关更多信息,请阅读OWASP SQL 注入预防备忘单

现在,如果你是高级用户,开始使用这个防御就可以了,但是,对于初学者来说,如果他们不能快速实现存储过程并准备好语句,那么最好尽可能过滤输入数据。

最后,让我们考虑一个用户在下面发送这个文本而不是输入他/她的用户名:

[1] UNION SELECT IF(SUBSTRING(Password,1,1)='2',BENCHMARK(100000,SHA1(1)),0) User,Password FROM mysql.user WHERE User = 'root'

无需任何准备好的语句和存储过程即可尽早检查此输入,但为了安全起见,在用户数据过滤和验证之后开始使用它们。

最后一点是检测需要更多努力和复杂性的意外行为;不建议将其用于普通的 Web 应用程序。

上述用户输入中的意外行为是 SELECT、UNION、IF、SUBSTRING、BENCHMARK、SHA 和 root。一旦检测到这些词,就可以避免输入。

更新 1:

有网友评论说这个帖子没用,OK!以下是OWASP.ORG 提供的内容:

主要防御:

选项 #1:使用准备好的语句(参数化查询)
选项 #2:使用存储过程
选项 #3:转义所有用户提供的输入

其他防御:

同时执行:最小权限
同时执行:白名单输入验证

您可能知道,声明一篇文章应该有一个有效的论据支持,至少有一个参考!否则,将被视为攻击和恶意索赔!

更新 2:

来自 PHP 手册,PHP:Prepared Statements - Manual

转义和 SQL 注入

绑定变量将被服务器自动转义。服务器在执行之前将它们的转义值插入到语句模板的适当位置。必须向服务器提供绑定变量类型的提示,以创建适当的转换。有关详细信息,请参阅 mysqli_stmt_bind_param() 函数。

服务器内的值的自动转义有时被认为是防止 SQL 注入的安全功能。如果输入值被正确转义,则可以使用非准备语句实现相同程度的安全性。

更新 3:

我创建了测试用例来了解 PDO 和 MySQLi 在使用准备好的语句时如何将查询发送到 MySQL 服务器:

PDO:

$user = "''1''"; // Malicious keyword
$sql = 'SELECT * FROM awa_user WHERE userame =:username';
$sth = $dbh->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
$sth->execute(array(':username' => $user));

查询日志:

    189 Query SELECT * FROM awa_user WHERE userame ='\'\'1\'\''
    189 Quit

MySQLi:

$stmt = $mysqli->prepare("SELECT * FROM awa_user WHERE username =?")) {
$stmt->bind_param("s", $user);
$user = "''1''";
$stmt->execute();

查询日志:

    188 Prepare   SELECT * FROM awa_user WHERE username =?
    188 Execute   SELECT * FROM awa_user WHERE username ='\'\'1\'\''
    188 Quit

很明显,准备好的语句也在转义数据,仅此而已。

正如上述声明中也提到的,

服务器内的值的自动转义有时被认为是防止 SQL 注入的安全功能。如果输入值被正确转义,则使用非准备语句可以实现相同程度的安全性

因此,这证明了intval()在发送任何查询之前对整数值进行数据验证是一个好主意。此外,在发送查询之前阻止恶意用户数据是一种正确有效的方法

请参阅此问题以获取更多详细信息:PDO 将原始查询发送到 MySQL 而 Mysqli 发送准备好的查询,两者都产生相同的结果

参考:

  1. SQL 注入备忘单
  2. SQL 注入
  3. 信息安全
  4. 安全原则
  5. 数据验证
于 2013-01-28T19:37:48.973 回答
178

安全警告:此答案不符合安全最佳实践。转义不足以防止 SQL 注入,请改用准备好的语句。使用下面概述的策略,风险自负。(另外,mysql_real_escape_string()在 PHP 7 中被删除。)

不推荐使用的警告:此时不推荐使用 mysql 扩展。我们建议使用PDO 扩展

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

  1. 使用mysql_real_escape_string(), 这是PHP中的一个预定义函数,并且此代码将反斜杠添加到以下字符:\x00\n\r\'和. 将输入值作为参数传递,以最大限度地减少 SQL 注入的机会。"\x1a
  2. 最先进的方法是使用 PDO。

我希望这能帮到您。

考虑以下查询:

$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";

这个问题对此有一些很好的答案。

我建议,使用 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");
于 2012-09-14T14:37:56.560 回答
172

一种简单的方法是使用像CodeIgniterLaravel这样的 PHP 框架,它们具有过滤和活动记录等内置功能,这样您就不必担心这些细微差别。

于 2013-06-25T19:00:36.200 回答
153

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

如果攻击者试图通过 PHP 的变量或 URL 的查询字符串侵入表单$_GET,如果它们不安全,您将能够捕获它们。

RewriteCond %{QUERY_STRING} ([0-9]+)=([0-9]+)
RewriteRule ^(.*) ^/track.php

因为1=1, 2=2, 1=2, 2=1, 1+1=2, 等是攻击者对 SQL 数据库的常见问题。也许它也被许多黑客应用程序使用。

但是您必须小心,您不能从您的站点重写安全查询。上面的代码给了你一个提示,重写或重定向(取决于你)特定于黑客的动态查询字符串到一个页面中,该页面将存储攻击者的IP 地址,甚至他们的 COOKIES、历史记录、浏览器或任何其他敏感信息信息,以便您以后可以通过禁止他们的帐户或联系当局来处理他们。

于 2013-04-04T08:15:30.653 回答
135

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

于 2014-03-20T00:17:35.683 回答
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);
于 2014-01-30T07:00:43.773 回答
127

不推荐使用的警告: 此答案的示例代码(如问题的示例代码)使用 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

于 2014-01-17T06:25:22.080 回答
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#-ish String.Format 运行语句,例如:

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

考虑到变量类型,它会逃脱。如果您尝试参数化表、列名,它将失败,因为它将每个字符串都放在引号中,这是一种无效的语法。

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

于 2014-02-18T20:38:27.203 回答