如何在PHP中阻止SQL注入?

时间:2008-09-12 23:55:01

标签: php mysql sql security sql-injection

如果在未修改SQL查询的情况下插入用户输入,则应用程序将容易受到SQL injection的攻击,如下例所示:

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

可以采取哪些措施来防止这种情况发生?

28 个答案:

答案 0 :(得分:8483)

使用预准备语句和参数化查询。这些是由数据库服务器与任何参数分开发送和解析的SQL语句。这样攻击者就无法注入恶意SQL。

你基本上有两个选择来实现这个目标:

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

    $stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
    
    $stmt->execute(array('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
    }
    
  3. 如果您要连接到MySQL以外的数据库,则可以参考特定于驱动程序的第二个选项(例如,pg_prepare()pg_execute()用于PostgreSQL。 PDO是通用选项。

    正确设置连接

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

    $dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');
    
    $dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
    $dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    

    在上面的示例中,错误模式并非严格必要,但建议添加。这样,当出现问题时,脚本不会以Fatal Error停止。它使开发人员有机会catch throw n为PDOException s的任何错误。

    什么是必需,但是,它是第一个setAttribute()行,它告诉PDO禁用模拟的预准备语句并使用真正的预处理语句。这可以确保PHP在将语句和值发送到MySQL服务器之前不会对其进行解析(使攻击者可能无法注入恶意SQL)。

    虽然你可以在构造函数的选项中设置charset,但重要的是要注意DSN中的'旧'版本的PHP(< 5.3.6)silently ignored the charset parameter

    说明

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

    这里重要的是参数值与编译语句结合,而不是SQL字符串。 SQL注入通过在创建SQL以发送到数据库时欺骗脚本来包含恶意字符串。因此,通过从参数中单独发送实际SQL,可以限制最终出现您不想要的内容的风险。使用预准备语句时发送的任何参数都将被视为字符串(尽管数据库引擎可能会进行一些优化,因此参数最终也可能作为数字)。在上面的示例中,如果$name变量包含'Sarah'; DELETE FROM employees,则结果只是搜索字符串"'Sarah'; DELETE FROM employees",而您最终不会得到an empty table

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

    哦,既然你问过如何为插入操作,这是一个例子(使用PDO):

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

    可以将预准备语句用于动态查询吗?

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

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

    // Value whitelist
    // $dir can only be 'DESC' otherwise it will be 'ASC'
    if (empty($dir) || $dir !== 'DESC') {
       $dir = 'ASC';
    }
    

答案 1 :(得分:1594)

  

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

     

安全警告:此答案与安全最佳做法不符。 Escaping is inadequate to prevent SQL injection,请改用预备语句。使用下面列出的策略需要您自担风险。 (另外,在PHP 7中删除了mysql_real_escape_string()。)

如果您使用的是最新版本的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方法,您将会遇到Polynomial在下面的评论中描述的问题。这种情况比较棘手,因为整数不会被引号括起来,所以你可以通过验证用户输入只包含数字来处理。
  • 可能还有其他我不知道的情况。您可能会发现this对于您可能遇到的一些更微妙的问题是一个有用的资源。

答案 2 :(得分:1022)

此处的每个答案仅涵盖部分问题。 事实上,我们可以动态添加四个不同的查询部分: -

  • 一个字符串
  • 一个数字
  • 标识符
  • 语法关键字。

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

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

通常,这种保护方法基于白名单

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

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

但是,还有另一种保护标识符的方法 - 转义。只要你引用了一个标识符,你就可以通过加倍来逃避内部反引号。

作为进一步的步骤,我们可以从准备好的语句中借用一些占位符(代表查询中的实际值的代理),并发明另一种类型的占位符 - 标识符占位符。

所以,长话短说:它是一个占位符,而不是准备好的声明可以被视为银弹。

因此,一般性建议可能被表述为 只要您使用占位符向查询添加动态部分(当然这些占位符已正确处理),您就可以确保查询是安全的

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

更新

尽管对SQL注入保护的最佳实践存在普遍的一致意见,但仍然存在很多不良实践。其中一些实践过于根深蒂固在PHP用户的脑海中。例如,在这个页面上(虽然大多数访问者看不到)超过80个已删除的答案 - 由于质量差或促进不良和过时的做法,所有这些都被社区删除。更糟糕的是,一些不好的答案不会被删除,而是会更加繁荣。

例如,there(1) are(2) still(3) many(4) answers(5),包括second most upvoted answer建议您手动字符串转义 - 过时的方法是被证明是不安全的。

或者有一个稍微好一点的答案表明只是another method of string formatting,甚至称它为最终灵丹妙药。当然,事实并非如此。这种方法并不比常规的字符串格式更好,但它保留了它的所有缺点:它只适用于字符串,和任何其他手动格式一样,它基本上是可选的,非强制性的措施,容易出现任何类型的人为错误。

我认为所有这一切都是因为一个非常古老的迷信,得到OWASPthe PHP manual等权威的支持,它们宣称“逃避”和防止SQL注入之间的平等。

无论PHP手册多年来所说的是什么, *_escape_string绝不会使数据安全,而且从来没有打算过。除了对字符串以外的任何SQL部分无用之外,手动转义是错误的,因为它是手动的,与自动化相反。

而OWASP更糟糕的是,强调逃避用户输入这完全是胡说八道:在注入保护的情况下应该没有这样的词。每个变量都有潜在危险 - 无论来源如何!或者,换句话说 - 每个变量都必须正确格式化以便放入查询中 - 无论来源是什么。这是重要的目的地。在开发人员开始将绵羊与山羊分开的那一刻(考虑某些特定变量是否“安全”)他/她迈出了他/她迈向灾难的第一步。更不用说即使是措辞也表明在入口点大量逃避,类似于非常神奇的引用功能 - 已经被鄙视,弃用和删除。

因此,与“转义”无关,准备好的语句确实可以防止SQL注入的措施(适用时)。

如果您仍然不相信,我在这里逐步解释 The Hitchhiker's Guide to SQL Injection prevention ,在那里我详细解释了所有这些问题,甚至完整地编写了一个部分致力于不良行为及其披露。

答案 3 :(得分:810)

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

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

通过使用PDO而不是mysql_mysqli_pgsql_函数,您可以使您的应用程序从数据库中抽象出来,在极少数情况下您需要切换数据库提供者。

答案 4 :(得分:600)

使用PDO并准备好查询。

$connPDO个对象)

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

答案 5 :(得分:532)

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

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

我的方法:

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

  • 如果你还需要整数 hex 的其他内容。如果你把它变成十六进制,你将完全逃脱所有输入。在C / C ++中有一个名为mysql_hex_string()的函数,在PHP中你可以使用bin2hex()

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

  • 这种十六进制方法通常在传输二进制数据时使用,但我认为没有理由不在所有数据上使用它来防止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,varchar,text,block,binary等
此外,如果您要插入空字符串,它的使用会有点复杂。您必须将其全部替换为'',否则您将收到错误。

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


Hex方法通常用作攻击

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

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

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

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

  

SELECT ... WHERE id = -1 union all来自information_schema.tables的select table_name

现在只提取表格结构:

  

SELECT ... WHERE id = -1 union all from information_schema.column select column_name其中table_name = 0x61727469636c65

然后只需选择想要的数据。不是很酷吗?

但是如果可注射部位的编码器会使其十六进制,则无法进行注射,因为查询将如下所示:SELECT ... WHERE id = UNHEX('2d312075...3635')

答案 6 :(得分:481)

  

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

     

安全警告:此答案与安全最佳做法不符。 Escaping is inadequate to prevent SQL injection,请改用预备语句。使用下面列出的策略需要您自担风险。 (另外,在PHP 7中删除了mysql_real_escape_string()。)

     

重要

     

防止SQL注入的最佳方法是使用准备好的语句 而不是转义,如the accepted answer所示。

     

Aura.SqlEasyDB等库允许开发人员更轻松地使用预准备语句。要详细了解为stopping SQL injection更好地准备好陈述的原因,请参阅this mysql_real_escape_string() bypassrecently fixed Unicode SQL Injection vulnerabilities in WordPress

注射预防 - 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 Injection Prevention 中找到更多详细信息。

答案 7 :(得分:445)

  

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

     

安全警告:此答案与安全最佳做法不符。 Escaping is inadequate to prevent SQL injection,请改用预备语句。使用下面列出的策略需要您自担风险。 (另外,在PHP 7中删除了mysql_real_escape_string()。)

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

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

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

答案 8 :(得分:370)

无论您最终使用什么,请确保检查您的输入是否已被magic_quotes或其他一些善意的垃圾所破坏,如有必要,请通过stripslashes或无论如何消毒它。

答案 9 :(得分:353)

  

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

     

安全警告:此答案与安全最佳做法不符。 Escaping is inadequate to prevent SQL injection,请改用预备语句。使用下面列出的策略需要您自担风险。 (另外,在PHP 7中删除了mysql_real_escape_string()。)

参数化查询和输入验证是可行的方法。在许多情况下,即使使用了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`");

在这两种情况下,您都无法使用'来保护封装。

Source意外的SQL注入(当转义不够时)

答案 10 :(得分:302)

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

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

答案 11 :(得分:288)

我赞成stored proceduresMySQL has had stored procedures support since 5.0)从安全的角度来看 - 优点是 -

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

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

答案 12 :(得分:283)

有许多方法可以防止SQL注入和其他SQL攻击。您可以在互联网上轻松找到它(谷歌搜索)。当然 PDO是一个很好的解决方案。但我想建议你从SQL注入中预防一些好的链接。

What is SQL injection and how to prevent

PHP manual for SQL injection

Microsoft explanation of SQL injection and prevention in PHP

和其他一些像Preventing SQL injection with MySQL and PHP

一样

现在,为什么你需要阻止查询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注入,则非常危险。

答案 13 :(得分:259)

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

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

  4. 图书馆示例:

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

    <强> P.S

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

    但是,虽然PDO和MySQLi都非常快,但是MySQLi表现得很好 基准测试速度微不足道 - 未准备好的情况下为2.5% 声明,准备好的约6.5%。

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

答案 14 :(得分:251)

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

$unsafe_variable = $_POST['user_id'];

$safe_variable = (int)$unsafe_variable ;

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

答案 15 :(得分:227)

如果您想利用缓存引擎,例如RedisMemcached,可能会选择DALMP。它使用纯MySQLi。检查一下:DALMP Database Abstraction Layer for MySQL using PHP.

此外,您可以在准备查询之前“准备”您的参数,以便您可以构建动态查询,并在最后具有完全准备好的语句查询。 DALMP Database Abstraction Layer for MySQL using PHP.

答案 16 :(得分:218)

对于那些不确定如何使用PDO(来自mysql_函数)的人,我创建了一个very, very simple PDO wrapper单个文件。它的存在表明,完成应用程序所需的所有常见事务是多么容易。适用于PostgreSQL,MySQL和SQLite。

基本上,请阅读while you read the manual,了解如何在现实生活中使用PDO功能,以便以想要的格式存储和检索值。

  

我想要一个专栏

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

我想要一个数组(key =&gt; 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));

答案 17 :(得分:216)

使用这个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

答案 18 :(得分:198)

一些在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

答案 19 :(得分:180)

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

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

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

FLUSH PRIVILEGES; 

有关flush的更多信息。

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

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

详细了解GRANT

答案 20 :(得分:169)

  

安全警告:此答案与安全最佳做法不符。 Escaping is inadequate to prevent SQL injection,请改用预备语句。使用下面列出的策略需要您自担风险。 (另外,在PHP 7中删除了mysql_real_escape_string()。)

     

已弃用警告:此时不推荐使用mysql扩展程序。我们建议使用 PDO扩展程序

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

  1. 使用mysql_real_escape_string() PHP中的预定义函数,此代码将反斜杠添加到以下字符:\x00\n,{{1 },\r\'"。将输入值作为参数传递,以最大限度地减少SQL注入的可能性。
  2. 最先进的方法是使用PDO。
  3. 我希望这会对你有所帮助。

    考虑以下问题:

    \x1a

    mysql_real_escape_string()在这里不会受到保护。如果在查询中的变量周围使用单引号('')可以保护您免受此攻击。以下是针对此的解决方案:

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

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

    我建议,使用PDO是最佳选择。

    修改

    自PHP 5.5.0起,不推荐使用

    $iId = (int) mysql_real_escape_string("1 OR 1=1"); $sSql = "SELECT * FROM table WHERE id = $iId";。使用mysqli或PDO。

    mysql_real_escape_string()的替代方法是

    mysql_real_escape_string()

    示例:

    string mysqli_real_escape_string ( mysqli $link , string $escapestr )
    

答案 21 :(得分:168)

关于许多有用的答案,我希望在这个帖子中添加一些值。 SQL注入是一种可以通过用户输入(由用户填充然后在查询中使用的输入)完成的攻击,SQL注入模式是正确的查询语法,而我们可以调用它:错误的查询由于不好的原因,我们假设可能存在是一个试图获取影响三个安全原则(机密性,完整性,可用性)的秘密信息(绕过访问控制)的坏人。

现在,我们的观点是防止SQL注入攻击等安全威胁,问题(如何使用PHP防止SQL注入攻击),使用用户输入时更加真实,数据过滤或清除输入数据这样的查询中的数据,使用PHP或任何其他编程语言不是这种情况,或者更多人推荐使用现代技术,如准备语句或当前支持SQL注入预防的任何其他工具,请考虑这些工具不再可用?您如何保护您的申请?

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

数据过滤(将不安全数据转换为安全数据) 考虑到PDOMySQLi不可用,您如何保护您的申请?你强迫我使用它们吗?除了PHP以外的其他语言怎么样?我更愿意提供一般性的想法,因为它可以用于更广泛的边界,而不仅仅是特定的语言。

  1. SQL用户(限制用户权限):最常见的SQL操作是(SELECT,UPDATE,INSERT),那么,为什么要给不需要它的用户提供UPDATE权限?例如,登录,搜索页面仅使用SELECT,那么,为什么在具有高权限的这些页面中使用数据库用户呢? 规则:不为所有权限创建一个数据库用户,对于所有SQL操作,您可以将(deluser,selectuser,updateuser)方案创建为用户名,以便于使用。
  2. 请参阅Principle of least privilege

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

    2. 使用其他工具:在这里,我也同意你的准备声明(参数化查询)和存储过程,这里的缺点是这些方法需要大多数用户不存在的高级技能,这里的基本思路是区分SQL查询和内部使用的数据,即使对于不安全的数据,这两种方法也可以使用,因为这里的用户输入数据不会向原始查询添加任何内容,例如(any或x = x)。 有关详细信息,请参阅OWASP SQL Injection Prevention Cheat Sheet

    3. 现在,如果您是高级用户,可以根据需要开始使用此防御,但是,对于初学者,如果他们无法快速实现存储过程并准备好语句,那么过滤输入会更好数据尽可能多。

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

      [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一旦检测到这些单词,就可以避免输入。

      UPDATE1:

      一位用户评论说这篇文章没用,好!以下是OWASP.ORG提供的内容:

        

      主要防御:
         
            选项#1:使用准备好的语句(参数化查询)
            选项#2:使用存储过程
            选项#3:转义所有用户提供的输入
         
        附加抗辩:
         
            还强制执行:最低权限
            同时执行:白名单输入验证

      正如您所知,声称一篇文章应该得到有效论证的支持,至少有一个参考!否则,它被视为攻击和不良声明!

      UPDATE2:

      从PHP手册PHP: Prepared Statements - Manual

        

      转义和SQL注入

           

      绑定变量将由服务器自动转义。该   服务器将其转义值插入到适当的位置   执行前的语句模板。必须提供一个提示   服务器为绑定变量的类型,以创建合适的   转换。有关更多信息,请参阅mysqli_stmt_bind_param()函数   信息。

           

      有时会自动转义服务器中的值   被认为是一种防止SQL注入的安全功能。相同   如果是,可以使用未准备好的陈述来实现安全程度   输入值正确转义。

      UPDATE3:

      我创建了一些测试用例,用于了解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
      

      很清楚,准备好的声明也在逃避数据,没有别的。

      正如上面的语句The automatic escaping of values within the server is sometimes considered a security feature to prevent SQL injection. The same degree of security can be achieved with non-prepared statements, if input values are escaped correctly中所提到的,这证明了在发送任何查询之前,诸如intval()之类的数据验证对于整数值是一个好主意,此外,在此之前防止恶意用户数据发送查询是正确且有效的方法

      请详细了解此问题:PDO sends raw query to MySQL while Mysqli sends prepared query, both produce the same result

      参考文献:

      1. SQL Injection Cheat Sheet
      2. SQL Injection
      3. Information security
      4. Security Principles
      5. Data validation

答案 22 :(得分:166)

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

答案 23 :(得分:140)

**警告:本答案中描述的方法仅适用于非常具体的场景,并且不安全,因为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 address,或甚至是他们的COOKIES,历史记录,浏览器或任何其他敏感信息,以便您稍后通过禁止其帐户或联系当局来处理它们。

答案 24 :(得分: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);

答案 25 :(得分:123)

一个好主意是使用'对象关系映射器',如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注入,还可以避免语法错误!还支持使用方法链接的模型集合,以便一次过滤或将操作应用于多个结果和多个连接。

答案 26 :(得分:120)

  

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

     

安全警告:此答案与安全最佳做法不符。 Escaping is inadequate to prevent SQL injection,请改用预备语句。使用下面列出的策略需要您自担风险。 (另外,在PHP 7中删除了mysql_real_escape_string()。)

使用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检查输入数据要好得多。

答案 27 :(得分:87)

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

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版本不会导致问题。