如何防止PHP中进行SQL注入?


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

如何防止这种情况的发生?

Answers:


8954

使用准备好的语句和参数化查询。这些是独立于任何参数发送到数据库服务器并由数据库服务器解析的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当出现问题时脚本不会以a停止。并且它为开发人员提供了解决catch任何thrown为PDOExceptions的错误的机会。

但是,第一行是强制性的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结果,则仅是搜索字符串"'Sarah'; DELETE FROM employees",并且最终不会得到空表

使用准备好的语句的另一个好处是,如果您在同一会话中多次执行同一条语句,则该语句仅被解析和编译一次,从而使您获得了一些速度上的提高。

哦,既然您询问如何插入,这是一个示例(使用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';
}

86
只是要添加一下,因为我在这里没有其他地方看到,另一道防线是Web应用程序防火墙(WAF),可以设置规则以查找sql​​注入攻击:
jkerak 2015年

37
另外,mysql_query的官方文档仅允许执行一个查询,因此除;以外的任何其他查询。被忽略。即使已经不赞成使用此功能,PHP 5.5.0下仍有许多系统可以使用此功能。php.net/manual/en/function.mysql-query.php
Randall Valenciano

12
这是一个坏习惯,但是是问题后的解决方案:如果您有旧的网站或应用程序正在遭受苦难,那么这不仅适用于SQL注入,而且适用于任何类型的注入(例如,F3框架v2中都有一个视图模板注入孔)从注入缺陷出发,一种解决方案是用引导程序中的转义值重新分配您的超全局预定义变量(例如$ _POST)的值。通过PDO,仍然有可能转义(也适用于当今的框架):substr($ pdo-> quote($ str,\ PDO :: PARAM_STR),1,-1)
Alix

15
这个答案缺乏对什么是预准备语句的解释-一件事-如果您在请求期间使用许多预准备语句,这对性能会产生影响,有时它会导致10倍的性能影响。更好的情况是使用不带参数绑定的PDO,但不准备语句。
donis

6
使用PDO是更好的,如果你使用的是直接查询请确保您使用的mysqli :: escape_string
卡西姆·伊塔尼

1652

不建议使用的警告: 此答案的示例代码(如问题的示例代码)使用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方法,则将遭受以下多项式描述的多项式问题。这种情况比较棘手,因为整数不会被引号引起来,因此可以通过验证用户输入仅包含数字来进行处理。
  • 可能还有其他我不知道的情况。您可能会发现是一些有用的资源,可以解决您遇到的一些更细微的问题。

1
使用mysql_real_escape_string就足够了还是我也必须使用参数化?
peiman F.

5
@peimanF。即使在本地项目上,也要保持使用参数化查询的良好做法。使用参数化查询,可以确保不会进行SQL注入。但是请记住,您应该对数据进行消毒处理,以防止伪造检索(例如XSS注入,例如将HTML代码放入文本中),htmlentities例如
Goufalite

2
@peimanF。参数化查询和绑定值的良好做法,但现在实际的转义字符串是好的
Richard Richard,

我理解mysql_real_escape_string()为了完整起见将其包含在内,但不喜欢首先列出最容易出错的方法。读者可能会很快抓住第一个例子。好东西,现在已不推荐使用:)
SteenSchütt18年

1
@SteenSchütt- mysql_*不推荐使用所有功能。它们被类似的 mysqli_*功能所代替,例如mysqli_real_escape_string
里克·詹姆斯

1072

这里的每个答案仅涵盖部分问题。实际上,我们可以动态地将四个查询部分添加到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语法关键字的问题(如ANDDESC和这样),但白名单,似乎在这种情况下,唯一的办法。

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

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

更新资料

尽管就SQL注入保护的最佳做法达成了普遍共识,但仍然存在许多不良做法。而且其中有些根深蒂固于PHP用户的思想中。例如,在此页面上(尽管对于大多数访问者不可见),已删除了80多个答案 -社区由于质量低劣或推广不良和过时的做法而将其删除。更糟糕的是,一些错误的答案并没有被删除,反而会蒸蒸日上。

例如,There (1) 有(2) still(3) many(4)个 答案(5),其中第二个最重要的答案是建议您手动转义字符串-一种过时的方法,被证明是不安全的。

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

我认为所有这些都是由于一种非常古老的迷信,并得到诸如OWASPPHP手册之类的权威的支持,该权威宣称在“转义”和防止SQL注入之间应保持平等。

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

OWASP更加糟糕,它强调逃避用户输入,这完全是胡说八道:在注入保护的上下文中不应有这样的措辞。每个变量都有潜在的危险-无论来源如何!或者,换句话说-每个变量都必须正确设置格式才能放入查询中-不管源是什么。重要的是目的地。当开发人员开始将绵羊与山羊分开时(考虑某个特定变量是否“安全”),他/她迈出了走向灾难的第一步。更不用说即使是措辞也建议在入口点进行大量转义,类似于非常讨厌的引号功能-已被轻视,不推荐使用和删除。

因此,与无论“逃离”,准备好的语句,确实从SQL注入保护措施(如适用)。


848

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

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

通过使用PDO而不是mysql_mysqli_pgsql_函数,可以使您的应用程序从数据库中抽象一些,这种情况很少发生,您必须切换数据库提供程序。


619

使用PDO和准备好的查询。

$conn是一个PDO对象)

$stmt = $conn->prepare("INSERT INTO tbl VALUES(:id, :name)");
$stmt->bindValue(':id', $id);
$stmt->bindValue(':name', $name);
$stmt->execute();

549

如您所见,人们建议您最多使用准备好的语句。没错,但是当每个进程执行一次查询时,将会有轻微的性能损失。

我当时面对这个问题,但我想我以非常复杂的方式解决了该问题-黑客用来避免使用引号的方式。我将其与模拟的准备好的语句结合使用。我用它来预防所有种类的可能的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')

十六进制是完美的转义。没有办法注入。

UNHEX函数和0x前缀之间的区别

评论中进行了一些讨论,所以我最后想澄清一下。这两种方法非常相似,但是在某些方面有所不同:

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

UNHEX()可在任何列上使用;您不必担心空字符串。


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

请注意,此十六进制方法通常用作SQL注入攻击,其中整数就像字符串一样,仅使用进行转义mysql_real_escape_string。这样就可以避免使用引号。

例如,如果您只是执行以下操作:

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

攻击可以很容易地注入你的力量。考虑从脚本返回的以下注入代码:

SELECT ... WHERE id = -1并都从information_schema.tables中选择table_name

现在只提取表结构:

SELECT ... WHERE id = -1并都从information_schema.column中选择column_name,其中table_name = 0x61727469636c65

然后,只需选择所需的任何数据即可。是不是很酷?

但是,如果可注入站点的编码器将其十六进制化,则不可能进行注入,因为查询将如下所示: SELECT ... WHERE id = UNHEX('2d312075...3635')


6
@SumitGupta是的,您做到了。MySQL不与串联,+但与CONCAT。并提高性能:我认为这不会影响性能,因为mysql必须解析数据,并且原点是字符串还是十六进制
都没关系

11
@YourCommonSense您不理解这个概念...如果您想在mysql中使用字符串,请像这样引用它,'root'或者0x726f6f74如果您想要一个数字并将其作为字符串发送,则可以将其十六进制,但是您可能会写成'42'而不是CHAR(42 )...十六进制的“ 42” 0x3432不是0x42
Zaffy 2013年

7
@YourCommonSense我无话可说...只是哈哈...如果您仍然想在数字字段上尝试十六进制,请参阅第二条注释。我打赌你会成功。
Zaffy 2013年

2
答案清楚地表明,它不适用于整数值,原因是bin2hex将传递的值转换为字符串(因此bin2hex(0)为0x30,而不是0x03)-这可能是使您感到困惑的部分。如果您遵循该方法,它将可以完美运行(至少在我的网站上,在5.1.x至5.6.x的debian机器上使用4个不同的mysql版本进行了测试)。毕竟,十六进制只是表示方式,而不是值;)
griffin

5
@YourCommonSense你还是不明白吗?您不能使用0x和concat,因为如果字符串为空,则将以错误结尾。如果你想简单的替代查询尝试这一个SELECT title FROM article WHERE id = UNHEX(' . bin2hex($_GET["id"]) . ')
Zaffy

499

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

安全警告:此答案与安全最佳做法不符。转义不足以防止SQL注入,请改用准备好的语句。使用以下概述的策略需要您自担风险。(此外,mysql_real_escape_string()已在PHP 7中删除。)

重要

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

有一些库,例如Aura.SqlEasyDB,使开发人员可以更轻松地使用准备好的语句。要了解更多有关为什么预处理语句更好地停止SQL注入的信息,请参阅WordPress中的mysql_real_escape_string()绕过方法最近修复的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注入预防中找到更多详细信息。


30
这是对旧版mysql扩展的最佳处理。对于新代码,建议您切换到mysqli或PDO。
阿尔瓦罗·冈萨雷斯

7
我不同意这种“防止这些攻击的特制功能”。我认为这样做的mysql_real_escape_string目的是允许为每个输入数据字符串建立正确的SQL查询。防止sql-injection是此函数的副作用。
sectus

4
您不使用函数编写正确的输入数据字符串。您只需编写不需要转义或已经转义的正确代码即可。mysql_real_escape_string()可能是出于您提到的目的而设计的,但是它唯一的价值是防止注入。
纳斯卡2014年

17
警告! mysql_real_escape_string() 并非绝对可靠
eggyal 2014年

9
mysql_real_escape_string现在已弃用,因此不再可行。将来将其从PHP中删除。最好继续学习PHP或MySQL专家的建议。
jww

461

您可以执行以下基本操作:

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

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


28
我已经尝试了您的示例,对我来说很好用。您能否清除“这不能解决所有问题”
Chinook

14
如果您不引用该字符串,那么它仍然可以注入。以$q = "SELECT col FROM tbl WHERE x = $safe_var";为例。设置$safe_var1 UNION SELECT password FROM users由于缺乏报价在这种情况下工作。也可以使用CONCAT和将字符串注入查询中CHR
多项式

4
@Polynomial完全正确,但是我认为这只是错误的用法。只要正确使用它,它肯定会起作用。
glglgl

22
警告! mysql_real_escape_string() 并非绝对可靠
eggyal 2014年

8
mysql_real_escape_string现在已弃用,因此不再可行。将来将其从PHP中删除。最好继续学习PHP或MySQL专家的建议。
jww

380

无论您最终使用什么,请确保检查输入内容是否尚未受到magic_quotes其他好意垃圾的干扰,如有必要,请对其进行遍历stripslashes或进行任何消毒。


11
确实; 在启用magic_quotes的情况下运行只会鼓励不良实践。但是,有时您无法始终将环境控制到该级别-您无权管理服务器,或者您的应用程序必须与(颤抖)依赖于这种配置的应用程序共存。出于这些原因,编写可移植的应用程序是很好的-尽管如果您确实控制部署环境,例如,因为它是内部应用程序,或者仅将在您的特定环境中使用,显然会浪费很多精力。
罗布

24
从PHP 5.4开始,被称为“魔术引号”的可憎之处已被杀死。并摆脱不良垃圾。
BryanH 2013年

363

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

安全警告:此答案与安全最佳做法不符。转义不足以防止SQL注入,请改用准备好的语句。使用以下概述的策略需要您自担风险。(此外,mysql_real_escape_string()已在PHP 7中删除。)

参数化查询和输入验证是必经之路。即使mysql_real_escape_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");

要么

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


2
如果采用输入验证技术,即根据一组定义的长度,类型和语法规则以及业务规则对用户输入进行身份验证,则可以防止SQL注入。
Josip Ivic

312

我认为,通常防止在PHP应用程序(或任何Web应用程序)中进行SQL注入的最佳方法是考虑应用程序的体系结构。如果防止SQL注入的唯一方法是记住每次使用数据库时都使用一种执行“正确的事情”的特殊方法或函数,那么您做错了。这样,在代码中的某个时候忘记忘记正确设置查询格式就只是时间问题了。

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


5
我认为您的第一段很重要。理解是关键。而且,每个人都不在公司工作。对于一大堆人来说,框架实际上与理解的想法背道而驰。在一定期限内工作时,与基本面保持亲密关系可能不会被重视,但是在那里做自己做的人喜欢弄脏自己的手。框架开发人员没有那么特权,以至于其他所有人必须屈服并假设他们从未犯过错误。做出决定的权力仍然很重要。谁说我的框架将来不会取代其他方案?
Anthony Rutledge

@AnthonyRutledge你是完全正确的。了解发生了什么以及为什么是非常重要的。但是,经过反复尝试,积极使用和开发的框架遇到并解决了许多问题并修补了许多安全漏洞的机会已经很高。查看源代码以了解代码质量是一个好主意。如果这是未经测试的烂摊子,那可能是不安全的。
Johannes Fahrenkrug

3
这里。这里。好点。但是,您是否同意许多人可以学习并学习采用MVC系统,但不是每个人都可以手动复制它(控制器和服务器)。在这一点上,我们可能走得太远。在我女朋友给我做的花生酱山核桃饼干加热之前,我需要了解我的微波炉吗?;-)
Anthony Rutledge

3
@AnthonyRutledge我同意!我认为用例也有所不同:我是在为个人主页构建照片库还是在构建在线银行Web应用程序?在后一种情况下,了解安全性的详细信息以及我正在使用的框架如何解决这些问题非常重要。
Johannes Fahrenkrug'1

3
嗯,安全例外是你自己做的必然结果。瞧,我倾向于愿意冒险冒险去破产。:-)开玩笑。只要有足够的时间,人们就可以学习制作一个相当安全的应用程序。太多人在忙着。他们举起手来,认为框架更安全。毕竟,他们没有足够的时间来测试和解决问题。此外,安全性是需要专门研究的领域。不仅仅是程序员通过了解算法和设计模式来深入了解。
安东尼·鲁特里奇

298

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

  1. 大多数数据库(包括MySQL)使用户访问仅限于执行存储过程。细粒度的安全访问控制对于防止特权攻击升级很有用。这样可以防止受感染的应用程序直接对数据库运行SQL。
  2. 他们从应用程序中提取原始SQL查询,因此应用程序可使用的数据库结构信息较少。这使人们更难理解数据库的底层结构并设计合适的攻击。
  3. 它们仅接受参数,因此存在参数化查询的优点。当然-IMO您仍然需要清理输入-特别是如果您在存储过程中使用动态SQL。

缺点是-

  1. 它们(存储过程)很难维护并且往往会快速繁殖。这使得管理它们成为一个问题。
  2. 它们不是非常适合动态查询-如果它们被构建为接受动态代码作为参数,那么许多优点就被抵消了。

297

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

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

PHP手册SQL注入

Microsoft用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注入,那是非常危险的。


267

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

  1. 考虑学习PDO(PHP数据对象)–它是数据库访问层,提供访问多个数据库的统一方法。
  2. 考虑学习MySQLi
  3. 使用本机PHP函数,例如:strip_tagsmysql_real_escape_string,或者如果是可变数字,则为just (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%。

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


mysqli不正确。第一个参数表示数据类型。
mickmackusa

257

如果可能,强制转换参数的类型。但这仅适用于int,bool和float等简单类型。

$unsafe_variable = $_POST['user_id'];

$safe_variable = (int)$unsafe_variable ;

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

3
这是我将使用“转义值”而不是准备好的语句的少数情况之一。而且整数类型转换非常有效。
HoldOffHunger


224

对于那些不确定如何使用PDO(来自mysql_功能)的人,我做了一个非常非常简单的PDO包装器,它是一个文件。它的存在表明执行应用程序需要完成的所有普通操作是多么容易。适用于PostgreSQL,MySQL和SQLite。

基本上,阅读它,而你阅读手册,看看如何把PDO功能,使用在现实生活中,使之易于存储和检索的格式值想要的。

我想要一个专栏

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

我想要一个array(key => value)结果(即用于创建一个选择框)

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

222

使用此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

220

在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顾名思义,and的用途是用于转义字符串中的特殊字符,因此它不会使整数安全。此功能的目的是防止破坏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

3
这个问题很笼统。上面有一些很好的答案,但大多数都建议您准备一些陈述。MySQLi异步不支持预备语句,因此sprintf在这种情况下看起来是一个不错的选择。
Dustin Graham

183

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


25
这个答案本质上是错误的,因为它无助于预防注射,而只是试图减轻后果。徒然。
您的常识

1
是的,它没有提供解决方案,但是您可以在事前避免发生这种情况。
Apurv Nerlekar '16

1
@Apurv如果我的目标是从您的数据库中读取私人信息,那么没有DELETE权限就没有任何意义。
Alex Holsgrove

1
@AlexHolsgrove:放轻松,我只是在建议一些好的方法来减轻后果。
Apurv Nerlekar '16

2
@Apurv您不想“减轻后果”,而是想尽一切可能防止它。公平地说,设置正确的用户访问权限很重要,但并不是OP真正要求的。
Alex Holsgrove '16

177

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

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:

一位用户评论说,这篇文章没有用,好!这是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. 资料验证

175

安全警告:此答案与安全最佳做法不符。转义不足以防止SQL注入,请改用准备好的语句。使用以下概述的策略需要您自担风险。(此外,mysql_real_escape_string()已在PHP 7中删除。)

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

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

  1. 使用的mysql_real_escape_string(),这是一个预先定义的函数PHP,这代码添加反斜杠以下字符:\x00\n\r\'"\x1a。将输入值作为参数传递,以最大程度地减少SQL注入的机会。
  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");


147

警告:此答案中描述的方法仅适用于非常特定的方案,并且不安全,因为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地址,甚至他们的菜谱,历史记录,浏览器或任何其他敏感信息信息,因此您以后可以通过禁止他们的帐户或与当局联系来与他们联系。


怎么了1-1=0?:)
拉普利·安德拉斯

@RápliAndrás某种([0-9\-]+)=([0-9]+)
5ervant

127

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

请解释oci_bind_by_name参数。
Jahanzeb Awan

127

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


124

不建议使用的警告: 此答案的示例代码(如问题的示例代码)使用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数组成员绝对没有意义
您的常识

21
警告! mysql_real_escape_string() 并非绝对可靠
eggyal 2014年

10
mysql_real_escape_string现在已弃用,因此不再可行。将来它将从PHP中删除。最好继续学习PHP或MySQL专家的建议。
jww 2015年

2
主题:不信任用户提交的数据。您期望的是带有特殊字符或布尔逻辑的垃圾数据,它们本身应该成为您可能正在执行的SQL查询的一部分。将$ _POST值仅保留为数据,而不保留SQL部分。
Bimal Poudel

88

几年前,我已经编写了这个小功能:

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如果替换版本包含这些令牌,则此版本不会引起问题。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.