如果是,为什么还有这么多成功的 SQL 注入?仅仅因为一些开发人员太笨,无法使用参数化语句?
12 回答
当文章谈论阻止 SQL 攻击的参数化查询时,他们并没有真正解释原因,通常是“它确实如此,所以不要问为什么”——可能是因为他们不了解自己。一个糟糕的教育者的一个明确迹象是不能承认他们不知道什么。但我离题了。当我说我发现完全可以理解时,感到困惑很简单。想象一个动态 SQL 查询
sqlQuery='SELECT * FROM custTable WHERE User=' + Username + ' AND Pass=' + password
所以一个简单的 sql 注入将只是将用户名作为 ' OR 1=1 - 这将有效地进行 sql 查询:
sqlQuery='SELECT * FROM custTable WHERE User='' OR 1=1-- ' AND PASS=' + password
这表示选择他们的用户名为空白 ('') 或 1=1 的所有客户,这是一个布尔值,等于 true。然后它使用 -- 注释掉查询的其余部分。所以这只会打印出所有的客户表,或者用它做任何你想做的事情,如果登录,它将使用第一个用户的权限登录,通常可以是管理员。
现在参数化查询以不同的方式进行,代码如下:
sqlQuery='SELECT * FROM custTable WHERE User=? AND Pass=?'
parameters.add("User", username)
parameters.add("Pass", password)
其中用户名和密码是指向相关输入用户名和密码的变量
现在,您可能会想,这根本不会改变任何事情。当然,您仍然可以在用户名字段中输入“Nobody OR 1=1'--”之类的内容,从而有效地进行查询:
sqlQuery='SELECT * FROM custTable WHERE User=Nobody OR 1=1'-- AND Pass=?'
这似乎是一个有效的论点。但是,你错了。
参数化查询的工作方式是将 sqlQuery 作为查询发送,并且数据库确切地知道该查询将做什么,然后才将用户名和密码仅作为值插入。这意味着它们不能影响查询,因为数据库已经知道查询会做什么。所以在这种情况下,它会寻找“Nobody OR 1=1'--”的用户名和一个空白密码,这应该是假的。
虽然这不是一个完整的解决方案,并且仍然需要进行输入验证,因为这不会影响其他问题,例如 XSS 攻击,因为您仍然可以将 javascript 放入数据库中。然后,如果将其读出到页面上,它将根据任何输出验证将其显示为普通 javascript。所以最好的办法仍然是使用输入验证,但使用参数化查询或存储过程来阻止任何 SQL 攻击。
我在对该问题的评论中发布的链接很好地解释了这个问题。我总结了我对为什么问题仍然存在的感受,如下:
刚入门的人可能对SQL注入一无所知。
有些人知道 SQL 注入,但认为转义是(唯一的?)解决方案。如果您对 进行快速 Google 搜索
php mysql query
,出现的第一个页面就是该mysql_query
页面,其中有一个示例显示将转义的用户输入插入到查询中。没有提到(至少我看不到)使用准备好的语句。正如其他人所说,那里有很多使用参数插值的教程,它仍然使用的频率并不奇怪。对参数化语句的工作方式缺乏了解。有些人认为这只是一种逃避价值的奇特手段。
其他人知道参数化语句,但不要使用它们,因为他们听说它们太慢了。我怀疑很多人都听说过参数化语句的速度非常慢,但实际上并没有自己做过任何测试。正如比尔卡尔文在他的演讲中指出的那样,在考虑使用准备好的语句时,性能差异很少被用作一个因素。一次准备,多次执行的好处似乎经常被遗忘,安全性和代码可维护性的改进也是如此。
有些在任何地方都使用参数化语句,但会插入未经检查的值,例如表和列名称、关键字和条件运算符。动态搜索,例如允许用户指定许多不同的搜索字段、比较条件和排序顺序的搜索,就是最好的例子。
使用 ORM 时的错误安全感。ORM 仍然允许对 SQL 语句部分进行插值 - 参见 5。
编程是一门大而复杂的学科,数据库管理是一门大而复杂的学科,安全性是一门大而复杂的学科。开发一个安全的数据库应用程序并不容易——即使是经验丰富的开发人员也可能会陷入困境。
stackoverflow 上的许多答案都无济于事。当人们编写使用动态 SQL 和参数插值的问题时,通常缺乏建议使用参数化语句的响应。有几次,有人反驳了我使用准备好的语句的建议——通常是因为感知到的性能开销不可接受。我严重怀疑那些提出这些问题中的大多数的人是否处于准备参数化语句所花费的额外几毫秒将对他们的应用程序产生灾难性影响的位置。
好问题。答案是随机的而不是确定的,我将尝试用一个小例子来解释我的观点。
网上有很多参考资料建议我们在查询中使用参数或使用带参数的存储过程以避免 SQL 注入 (SQLi)。我将向您展示存储过程(例如)不是对抗 SQLi 的魔法棒。责任仍然在程序员身上。
考虑以下 SQL Server 存储过程,它将从表“用户”中获取用户行:
create procedure getUser
@name varchar(20)
,@pass varchar(20)
as
declare @sql as nvarchar(512)
set @sql = 'select usrID, usrUName, usrFullName, usrRoleID '+
'from Users '+
'where usrUName = '''+@name+''' and usrPass = '''+@pass+''''
execute(@sql)
您可以通过将用户名和密码作为参数传递来获得结果。假设密码是自由文本(只是为了简单起见),正常调用将是:
DECLARE @RC int
DECLARE @name varchar(20)
DECLARE @pass varchar(20)
EXECUTE @RC = [dbo].[getUser]
@name = 'admin'
,@pass = '!@Th1siSTheP@ssw0rd!!'
GO
但是这里我们有一个程序员在存储过程中使用的不好的编程技术,所以攻击者可以执行以下操作:
DECLARE @RC int
DECLARE @name varchar(20)
DECLARE @pass varchar(20)
EXECUTE @RC = [TestDB].[dbo].[getUser]
@name = 'admin'
,@pass = 'any'' OR 1=1 --'
GO
上述参数将作为参数传递给存储过程,最终将执行的 SQL 命令为:
select usrID, usrUName, usrFullName, usrRoleID
from Users
where usrUName = 'admin' and usrPass = 'any' OR 1=1 --'
..这将从用户那里取回所有行
这里的问题是,即使我们遵循“创建存储过程并将字段作为参数传递给搜索”的原则,SQLi 仍然会执行。这是因为我们只是在存储过程中复制了我们糟糕的编程习惯。解决问题的方法是重写我们的存储过程如下:
alter procedure getUser
@name varchar(20)
,@pass varchar(20)
as
select usrID, usrUName, usrFullName, usrRoleID
from Users
where usrUName = @name and usrPass = @pass
我想说的是,开发人员必须首先了解什么是 SQLi 攻击以及如何执行,然后相应地保护他们的代码。盲目地遵循“最佳实践”并不总是更安全的方式……也许这就是为什么我们有这么多“最佳实践”——失败!
是的,使用准备好的语句可以阻止所有 SQL 注入,至少在理论上是这样。在实践中,参数化语句可能不是真正的准备语句,例如PDO
在 PHP 中默认模拟它们,因此它容易受到边缘情况攻击。
如果您使用的是真正的准备好的语句,那么一切都是安全的。好吧,至少只要您不将不安全的 SQL 连接到查询中,作为对无法准备表名的反应。
如果是,为什么还有这么多成功的 SQL 注入?仅仅因为一些开发人员太笨,无法使用参数化语句?
是的,教育是这里的重点,还有遗留代码库。不幸的是,许多教程都使用转义,并且不能轻易地从网络上删除。
我在编程中避免绝对;总是有例外。我强烈推荐存储过程和命令对象。我的大部分背景是使用 SQL Server,但我有时会使用 MySql。存储过程有很多优点,包括缓存查询计划;是的,这可以通过参数和内联 SQL 来完成,但这为注入攻击开辟了更多可能性,并且无助于关注点分离。对我来说,保护数据库也容易得多,因为我的应用程序通常只有所述存储过程的执行权限。如果没有直接的表/视图访问,注入任何东西都会更加困难。如果应用程序用户受到损害,则只有一个权限才能准确执行预定义的内容。
我的两分钱。
SQL 注入是更大的代码注入问题的一个子集,其中数据和代码通过同一通道提供,并且数据被误认为是代码。参数化查询通过使用关于什么是数据和什么是代码的上下文形成查询来防止这种情况发生。
在某些特定情况下,这还不够。在许多 DBMS 中,可以使用存储过程动态执行 SQL,从而在 DBMS 级别引入 SQL 注入缺陷。使用参数化查询调用这样的存储过程不会阻止过程中的 SQL 注入被利用。另一个例子可以在这篇博文中看到。
更常见的是,开发人员错误地使用了该功能。正确完成后,代码通常看起来像这样:
db.parameterize_query("select foo from bar where baz = '?'", user_input)
一些开发人员会将字符串连接在一起,然后使用参数化查询,这实际上并没有做出上述数据/代码区分,从而提供我们正在寻找的安全保证:
db.parameterize_query("select foo from bar where baz = '" + user_input + "'")
正确使用参数化查询可以提供非常强大但并非不可穿透的 SQL 注入攻击保护。
我不会说“笨”。
我认为教程是问题所在。大多数 SQL 教程、书籍,无论是用内联值解释 SQL,都根本没有提到绑定参数。从这些教程中学习的人没有机会正确地学习它。
要保护您的应用程序免受 SQL 注入,请执行以下步骤:
步骤 1. 约束输入。步骤 2. 使用存储过程的参数。步骤 3. 使用动态 SQL 的参数。
因为大多数代码都没有考虑到安全性和管理,如果在添加功能(尤其是可以出售的可见的东西)和安全性/稳定性/可靠性(这是一个更难卖的东西)之间做出选择,他们几乎总是会选择前任的。只有当它成为问题时,安全才是一个问题。
参数化语句可以停止所有 SQL 注入吗?
是的,只要您的数据库驱动程序为每个可能的 SQL 文字提供一个占位符。大多数准备好的语句驱动程序都没有。比如说,您永远找不到字段名称或值数组的占位符。这将使开发人员退回到手动定制查询,使用连接和手动格式化。与预期的结果。
这就是我为 PHP 制作 Mysql 包装器的原因,它支持大多数可以动态添加到查询中的文字,包括数组和标识符。
如果是,为什么还有这么多成功的 SQL 注入?仅仅因为一些开发人员太笨,无法使用参数化语句?
如您所见,实际上不可能将所有查询参数化,即使您不笨。
首先我回答你的第一个问题:是的,据我所知,通过使用参数化查询,SQL 注入将不再可能。关于你下面的问题,我不确定,只能给你我的看法:
我认为通过将一些不同的部分(甚至可能依赖于一些逻辑检查)与要插入的值连接起来,“只”编写 SQL 查询字符串会更容易。它只是创建查询并执行它。另一个优点是您可以打印(回显、输出或其他)sql 查询字符串,然后使用此字符串手动查询数据库引擎。
使用准备好的语句时,您总是至少还有一步: 您必须构建查询(当然包括参数) 您必须在服务器上准备查询 您必须将参数绑定到您想要的实际值用于您的查询 您必须执行查询。
这需要更多的工作(而且不是那么简单的编程),特别是对于一些“快速而肮脏”的工作,这些工作通常被证明是非常长寿的......
最好的祝福,
盒子
即使在整个 Web 应用程序自己的代码中正确使用准备好的语句,如果数据库代码组件以不安全的方式从用户输入构造查询,SQL 注入缺陷仍可能存在。以下是在@name 参数中易受 SQL 注入攻击的存储过程示例:
CREATE PROCEDURE show_current_orders
(@name varchar(400) = NULL)
AS
DECLARE @sql nvarchar(4000)
SELECT @sql = ‘SELECT id_num, searchstring FROM searchorders WHERE ‘ +
‘searchstring = ‘’’ + @name + ‘’’’;
EXEC (@sql)
GO
即使应用程序以安全的方式将用户提供的名称值传递给存储过程,过程本身也会将其直接连接到动态查询中,因此很容易受到攻击。