如何防止PHP中的SQL注入?

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

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

2087593 次浏览

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

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

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

安全警告:此答案不符合安全最佳实践。脱逃不足以防止SQL注射,请改用准备好的声明。使用下面概述的策略,风险自负。

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

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

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

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

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

你想要阅读的关键函数将是#0

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

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

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

无论您使用哪个数据库,避免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. 使用MySQL i(对于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以外的数据库,则可以参考特定于驱动程序的第二个选项(例如,PostgreSQL的pg_prepare()pg_execute())。PDO是通用选项。


正确设置连接

PDO

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

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

在上面的示例中,错误模式不是严格必要的,但建议把它加进去。这样PDO将通过抛出PDOException来通知您所有MySQL错误。

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

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

MySQL

对于mysqli,我们必须遵循相同的例程:

mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); // error reporting$dbConnection = new mysqli('127.0.0.1', 'username', 'password', 'test');$dbConnection->set_charset('utf8mb4'); // charset

补充说明

传递给prepare的SQL语句由数据库服务器解析和编译。通过指定参数(?或上面示例中的: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';}

使用PDO和准备好的查询。

$connPDO对象)

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

无论你最终使用什么,确保你检查你的输入没有被magic_quotes或其他一些善意的垃圾破坏,如果有必要,通过stripslashes或其他什么来净化它。

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

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

缺点是——

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

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

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

重要

防止SQL注射的最佳方法是使用准备的发言稿而不是逃避,如公认的答案所示。

有一些库,例如Aura. sqlEasyDB,允许开发人员更轻松地使用准备好的语句。要了解有关为什么准备好的语句在停止SQL注射更好的更多信息,请参阅这个#0旁路最近修复了WordPress中的UnicodeSQL注入漏洞

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

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

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

参数化查询和输入验证是要走的路。有许多情况下可能会发生SQL注入,即使使用了mysql_real_escape_string()

这些例子容易受到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注射(当逃跑还不够时)

这里的每个答案都只涵盖了问题的一部分。事实上,有个不同的查询部分,我们可以动态添加到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多个已删除的答案-由于质量差或宣传不良和过时的做法而被社区删除。更糟糕的是,一些糟糕的答案没有被删除,而是蓬勃发展。

例如,有(1)是(2)还(3)很多(4)答案(5),包括得票第二的答案建议您手动字符串转义-这种过时的方法被证明是不安全的。

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

我认为这一切都是因为一个非常古老的迷信,得到了像OWASP的php手册这样的权威的支持,它宣称任何“逃避”和保护免受SQL注射之间都是平等的。

不管PHP手册说了多少年,#0并不意味着数据安全和从来没有打算过。除了对字符串以外的任何SQL部分无用之外,手动转义是错误的,因为它是手动的,与自动化相反。

OWASP让情况变得更糟,强调转义用户输入完全是胡说八道:在注入保护的上下文中不应该有这样的词。每个变量都是潜在危险的——无论来源!或者,换句话说,每个变量都必须被正确格式化才能放入查询——无论来源是什么。重要的是目的地。当开发人员开始分崩离析的那一刻(考虑某个特定变量是否“安全”),他/她就迈出了走向灾难的第一步。更不用说,即使是措辞也建议在切入点大量逃逸,类似于非常神奇的报价功能-已经被鄙视,弃用和删除。

因此,与任何“逃逸”不同,准备语句确实保护SQL注入的措施(适用时)。

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

$unsafe_variable = $_POST['user_id'];
$safe_variable = (int)$unsafe_variable ;
mysqli_query($conn, "INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

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

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

有很多方法可以防止SQL注射和其他SQL黑客。你可以很容易地在互联网上找到它(谷歌搜索)。当然PDO是一个很好的解决方案。但我想建议你一些很好的链接预防SQL注射。

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

PHPSQL注入手册

微软解释PHP中的SQL注入和预防

还有一些像防止SQL注入MySQL和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注入,这是非常危险的。

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

警告:此时删除了mysql扩展。我们建议使用PDO扩展

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

安全警告:此答案不符合安全最佳实践。脱逃不足以防止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");

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

基本上,阅读它在你阅读手册的时候以了解如何将PDO函数用于现实生活中,以使以想要的格式存储和检索值变得简单。

我要一个专栏

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

我想要一个数组(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'));

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

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

我的方法:

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

  • 如果您对整数把它还有其他期望。如果您对其进行十六进制运算,您将完美地转义所有输入。在C/C++有一个名为#0的函数,在PHP中您可以使用#1

    不要担心转义字符串的大小将是其原始长度的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前缀只能用于charvarchartextblockbinary等数据列。
此外,如果你要插入一个空字符串,它的使用有点复杂。你必须完全用''替换它,否则你会得到一个错误。

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

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

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

图书馆示例:

----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 trainersWHERE name = ?AND email = ?AND last_login > ?');
$query->bind_param('first_param', 'second_param', $mail, time() - 3600);$query->execute();

P. S

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

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

了解更多关于赠款

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

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

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

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

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

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

  1. SQLuser(限制用户权限):最常见的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)。

有关更多信息,请阅读OWASPSQL注射预防备忘单

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

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

[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:准备好的语句-手册

逃逸和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. 数据验证

警告:此答案中描述的方法仅适用于非常特定的场景,并且不安全,因为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,历史,浏览器或任何其他敏感信息,所以你可以稍后通过禁止他们的帐户或联系当局来处理他们。

安全警告:此答案不符合安全最佳实践。脱逃不足以防止SQL注射,请改用准备好的声明

在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语句中的字符串,以及它可能对数据库造成的损害。

示例:

$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

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

不建议使用的警告:此答案的示例代码(如问题的示例代码)使用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的输入数据要好得多。

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

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

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

一个好主意是使用像习语这样的对象关系映射器

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