有没有更好的方法来对抗 SQL 注入?
Is there a better way to combat SQL Injection?
我已经多次观看 Computerphile 关于这个主题的视频(对于任何想要的人,这是 link:https://www.youtube.com/watch?v=_jKylhJtPmI)。他提供了一些关于如何对抗 SQL Injection
并使您的应用程序更有效的非常好的建议。这些是他视频中的要点:
- 不要直接使用未受保护的 SQL 命令,因为黑客可以通过这种方式执行 SQL 注入、窃取、修改甚至删除您的数据。
- 一个好的方法是使用
mysql_real_escape_string(String s)
函数。这基本上在每个危险字符 (/,", {, }, etc
) 的开头放置了一个斜线 (/
)。所以基本上这会使字符串中的引号或斜线变得无用。
最好的办法是使用准备好的语句。所以,你基本上说:
SELECT * FROM USERS WHERE username = ?
稍后您将问号替换为您要输入的用户名字符串。这样做的好处是不会混淆 PHP 或任何其他容错语言,并且使用这个简单且(有点古怪)优雅的解决方案只是说用字符串替换它并告诉语言所给的是只是一个字符串,仅此而已。
这很好,但是这个视频真的过时了。它于 2013 年问世,此后出现了许多新技术。因此,我尝试在互联网上搜索以查找是否有任何新方法或者是否就是这种方法。但问题是要么我找不到它,要么我发现了一些非常混乱的东西。
所以,我的问题是:是否有更好和增强的方法来对抗已经引入的 SQL 注入,或者准备好的语句是否仍然是常态并且它们是否容易受到任何类型的攻击?
在大多数将动态数据与 SQL 查询组合的示例中,参数绑定仍然是最佳解决方案。
你应该明白为什么。它不只是为你做一个字符串替换。你可以自己做。
之所以有效,是因为它将动态值与 SQL 解析步骤分开。 RDBMS 在 prepare()
:
期间解析 SQL 语法
$stmt = $pdo->prepare("SELECT * FROM USERS WHERE username = ?");
在这一点之后,RDBMS 知道 ?
必须只是一个单个标量值。没有别的。不是值列表,不是列名,不是表达式,不是子查询,不是第二个 SELECT 查询的 UNION,等等。
然后在执行步骤中发送要绑定到该占位符的值。
$stmt->execute( [ "taraiordanov" ] );
该值被发送到 RDBMS 服务器,它在查询中占据一席之地,但仅作为 值,然后可以执行查询。
这允许您使用插入的不同值多次执行查询。即使 SQL 解析器只需要解析一次查询。它会记住如何将新值插入到原始准备好的 SQL 查询中,因此您可以 execute()
任意多次:
$stmt->execute( [ "hpotter" ] );
$stmt->execute( [ "hgranger" ] );
$stmt->execute( [ "rweasley" ] );
...
准备好的语句是最好的吗?对,他们是。该建议来自 2013 年并不重要,它仍然是正确的。实际上,这个关于 SQL 的功能可以追溯到更早的时候。
那么查询参数是防御 SQL 注入的万无一失的方法吗?是的,如果您需要将变量组合为 SQL 中的 value。也就是说,您打算在查询中替换参数,否则您将使用带引号的字符串文字、带引号的日期文字或数字文字。
但是您可能还需要对查询执行其他操作。有时您需要根据应用程序中的条件逐个构建 SQL 查询。例如,如果您想搜索 username
但有时还要在搜索 last_login
日期时添加一个字词怎么办?参数不能将整个新术语添加到搜索中。
这是不允许的:
$OTHER_TERMS = "and last_login > '2019-04-01'";
$stmt = $pdo->prepare("SELECT * FROM USERS WHERE username = ? ?");
$stmt->execute( [ "taraiordanov", $OTHER_TERMS ] ); // DOES NOT WORK
如果你想让用户请求对结果进行排序,并且你想让用户选择按哪一列排序,是升序还是降序?
$stmt = $pdo->prepare("SELECT * FROM USERS WHERE username = ? ORDER BY ? ?");
$stmt->execute( [ "taraiordanov", "last_login", "DESC" ] ); // DOES NOT WORK
在这些情况下,您必须在 SQL 字符串 之前 prepare()
中放入列名称和查询术语的语法。您只需要格外小心,不要让不受信任的输入污染您放入查询中的动态部分。也就是说,确保它基于您在代码中完全控制的字符串值,而不是来自应用程序外部的任何内容,例如用户输入或文件或调用 API.
的结果
评论:
Martin 添加的想法有时称为白名单。我将以更具可读性的方式写出 Martin 的示例:
switch ($_GET['order']) {
case "desc":
$sqlOrder = "DESC";
break;
default:
$sqlOrder = "ASC";
break;
}
我用 default
替换了 Martin 的 case "asc"
因为如果用户输入的是任何东西 else —— 即使是恶意的东西 —— 唯一可以发生的是任何其他输入将默认为 SQL order ASC
。
这意味着只有两种可能的结果,ASC
或 DESC
。一旦您的代码完全控制了可能的值,并且您知道这两个值都是安全的,那么您可以将该值插入到您的 SQL 查询中。
简而言之:始终牢记 $_GET
和 $_POST
可能包含恶意内容的假设。客户很容易将他们想要的任何东西放入请求中。它们不受 HTML 表单中的值限制。
牢记该假设,防御性地编写代码。
另一个提示:许多人认为 $_GET
和 $_POST
中的客户端输入是您需要防范的唯一输入。这不是真的!任何输入源都可能包含有问题的内容。例如,读取文件并在 SQL 查询中使用该文件,或调用 API。
即使是之前已安全插入数据库的数据,如果使用不当,也可能会引入 SQL 注入。
我已经多次观看 Computerphile 关于这个主题的视频(对于任何想要的人,这是 link:https://www.youtube.com/watch?v=_jKylhJtPmI)。他提供了一些关于如何对抗 SQL Injection
并使您的应用程序更有效的非常好的建议。这些是他视频中的要点:
- 不要直接使用未受保护的 SQL 命令,因为黑客可以通过这种方式执行 SQL 注入、窃取、修改甚至删除您的数据。
- 一个好的方法是使用
mysql_real_escape_string(String s)
函数。这基本上在每个危险字符 (/,", {, }, etc
) 的开头放置了一个斜线 (/
)。所以基本上这会使字符串中的引号或斜线变得无用。 最好的办法是使用准备好的语句。所以,你基本上说:
SELECT * FROM USERS WHERE username = ?
稍后您将问号替换为您要输入的用户名字符串。这样做的好处是不会混淆 PHP 或任何其他容错语言,并且使用这个简单且(有点古怪)优雅的解决方案只是说用字符串替换它并告诉语言所给的是只是一个字符串,仅此而已。
这很好,但是这个视频真的过时了。它于 2013 年问世,此后出现了许多新技术。因此,我尝试在互联网上搜索以查找是否有任何新方法或者是否就是这种方法。但问题是要么我找不到它,要么我发现了一些非常混乱的东西。
所以,我的问题是:是否有更好和增强的方法来对抗已经引入的 SQL 注入,或者准备好的语句是否仍然是常态并且它们是否容易受到任何类型的攻击?
在大多数将动态数据与 SQL 查询组合的示例中,参数绑定仍然是最佳解决方案。
你应该明白为什么。它不只是为你做一个字符串替换。你可以自己做。
之所以有效,是因为它将动态值与 SQL 解析步骤分开。 RDBMS 在 prepare()
:
$stmt = $pdo->prepare("SELECT * FROM USERS WHERE username = ?");
在这一点之后,RDBMS 知道 ?
必须只是一个单个标量值。没有别的。不是值列表,不是列名,不是表达式,不是子查询,不是第二个 SELECT 查询的 UNION,等等。
然后在执行步骤中发送要绑定到该占位符的值。
$stmt->execute( [ "taraiordanov" ] );
该值被发送到 RDBMS 服务器,它在查询中占据一席之地,但仅作为 值,然后可以执行查询。
这允许您使用插入的不同值多次执行查询。即使 SQL 解析器只需要解析一次查询。它会记住如何将新值插入到原始准备好的 SQL 查询中,因此您可以 execute()
任意多次:
$stmt->execute( [ "hpotter" ] );
$stmt->execute( [ "hgranger" ] );
$stmt->execute( [ "rweasley" ] );
...
准备好的语句是最好的吗?对,他们是。该建议来自 2013 年并不重要,它仍然是正确的。实际上,这个关于 SQL 的功能可以追溯到更早的时候。
那么查询参数是防御 SQL 注入的万无一失的方法吗?是的,如果您需要将变量组合为 SQL 中的 value。也就是说,您打算在查询中替换参数,否则您将使用带引号的字符串文字、带引号的日期文字或数字文字。
但是您可能还需要对查询执行其他操作。有时您需要根据应用程序中的条件逐个构建 SQL 查询。例如,如果您想搜索 username
但有时还要在搜索 last_login
日期时添加一个字词怎么办?参数不能将整个新术语添加到搜索中。
这是不允许的:
$OTHER_TERMS = "and last_login > '2019-04-01'";
$stmt = $pdo->prepare("SELECT * FROM USERS WHERE username = ? ?");
$stmt->execute( [ "taraiordanov", $OTHER_TERMS ] ); // DOES NOT WORK
如果你想让用户请求对结果进行排序,并且你想让用户选择按哪一列排序,是升序还是降序?
$stmt = $pdo->prepare("SELECT * FROM USERS WHERE username = ? ORDER BY ? ?");
$stmt->execute( [ "taraiordanov", "last_login", "DESC" ] ); // DOES NOT WORK
在这些情况下,您必须在 SQL 字符串 之前 prepare()
中放入列名称和查询术语的语法。您只需要格外小心,不要让不受信任的输入污染您放入查询中的动态部分。也就是说,确保它基于您在代码中完全控制的字符串值,而不是来自应用程序外部的任何内容,例如用户输入或文件或调用 API.
评论:
Martin 添加的想法有时称为白名单。我将以更具可读性的方式写出 Martin 的示例:
switch ($_GET['order']) {
case "desc":
$sqlOrder = "DESC";
break;
default:
$sqlOrder = "ASC";
break;
}
我用 default
替换了 Martin 的 case "asc"
因为如果用户输入的是任何东西 else —— 即使是恶意的东西 —— 唯一可以发生的是任何其他输入将默认为 SQL order ASC
。
这意味着只有两种可能的结果,ASC
或 DESC
。一旦您的代码完全控制了可能的值,并且您知道这两个值都是安全的,那么您可以将该值插入到您的 SQL 查询中。
简而言之:始终牢记 $_GET
和 $_POST
可能包含恶意内容的假设。客户很容易将他们想要的任何东西放入请求中。它们不受 HTML 表单中的值限制。
牢记该假设,防御性地编写代码。
另一个提示:许多人认为 $_GET
和 $_POST
中的客户端输入是您需要防范的唯一输入。这不是真的!任何输入源都可能包含有问题的内容。例如,读取文件并在 SQL 查询中使用该文件,或调用 API。
即使是之前已安全插入数据库的数据,如果使用不当,也可能会引入 SQL 注入。