参数化语句能够阻止所有的SQL注入吗?

93

如果是的话,为什么还有那么多成功的SQL注入攻击呢?只是因为一些开发人员没有使用参数化语句吗?


6
这是一个很好的问题,但回答非常糟糕(就我评论的时间而言)。 - Ibu
我希望有至少15k声望或丰富经验的人能够为这个问题提供有价值的意见。 - Ibu
4
请查看Bill KarwinSql Injection Myths and Fallacies talkslides,以获取更多关于此主题的信息。他解释了什么是SQL注入,如何逃逸通常不够好,并且如何破坏存储过程和参数化语句。 - Mike
2
另外,还可以查看一些 Bill Karwin 对类似问题的答案: 什么是SQL注入? - Mike
12个回答

85

当文章谈论参数化查询可以防止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(布尔类型,等同于真)的客户,然后使用--注释掉查询的其余部分。所以如果登录,它将使用第一个用户的权限进行登录,通常情况下是管理员权限。这将打印出整个客户表,或者对其进行任何操作。

而参数化查询则使用以下代码:

sqlQuery='SELECT * FROM custTable WHERE User=? AND Pass=?'

parameters.add("User", username)
parameters.add("Pass", password)

其中,username和password是指向相关输入的变量。

现在,在这一点上,你可能会想,这根本没有改变什么。当然,你仍然可以在用户名字段中输入类似于Nobody OR 1=1'--的内容,从而有效地进行查询:

sqlQuery='SELECT * FROM custTable WHERE User=Nobody OR 1=1'-- AND Pass=?'

这个看起来像是一个合理的论点。但是,你错了。

参数化查询的工作方式是将 sqlQuery 作为查询发送,数据库已经准确知道这个查询会做什么,然后只会将用户名和密码作为值插入。这意味着它们无法影响查询,因为数据库已经知道查询会做什么。所以在这种情况下,它将查找用户名为 "Nobody OR 1=1'--" 和一个空密码,应该返回 false。

不过这并不是一个完整的解决方案,仍需要进行输入验证,因为这不会影响其他问题,例如 XSS 攻击,因为您仍然可以将 JavaScript 放入数据库中。然后如果在页面中读取这些内容,根据任何输出验证,它将显示为正常的 JavaScript。因此,真正最好的方法仍然是使用输入验证,但使用参数化查询或存储过程来防止任何 SQL 攻击。


1
这对我寻找的内容有很大帮助,但您能否进一步解释一下您将如何进行“输入验证”? 您还提到了其他可能发生的攻击,例如XSS,但您能否解释一下它是如何发生的? 因此,基本上,我们如何完全防止SQL注入,或者我们正在考虑所有类型的注入? 谢谢。 - XaolingBao
3
鉴于有很多人问如何使用参数化语句,看到很少甚至没有其他人像您那样详细解释过这个问题,真是令人震惊。感谢您写了这样清晰易懂的解释,并且给出了一个相当直观的例子。 - daOnlyBG
太棒了。正如人们所说的,一个例子胜过千言万语! - Drenai

74

我在评论中发布的链接已经很好地解释了这个问题。以下是我对问题仍然存在的原因的总结:

  1. 那些刚开始学习的人可能没有意识到SQL注入。

  2. 有人知道SQL注入,但认为转义是(唯一的?)解决方案。如果你快速搜索 php mysql query,第一页出现的是 mysql_query 页面,在该页面上有一个例子显示将经过转义的用户输入插入查询中。至少我看不到任何提到使用预处理语句的内容。正如其他人所说,有许多教程使用参数插值,所以它仍然被广泛使用并不令人惊讶。

  3. 缺乏对参数化语句工作原理的理解。有些人认为这只是一种高级的转义值的方法。

  4. 其他人知道预处理语句,但不使用它们,因为他们听说它们太慢了。我怀疑许多人听说过预处理语句的性能极差,但实际上并没有进行任何测试。正如Bill Karwin在他的演说中指出的那样,性能差异很少作为考虑使用预处理语句的因素。prepare once, execute many 的好处经常被忽视,以及安全性和代码可维护性的改进。

  5. 有些人在任何地方都使用参数化语句,但是插值未经检查的值,例如表和列名、关键字和条件运算符。动态搜索,例如允许用户指定多个不同的搜索字段、比较条件和排序顺序,就是这样的一个典型示例。

  6. 当使用ORM时,会有一种虚假的安全感。ORM仍然允许SQL语句部分的插值 - 参见5。

  • 编程是一个庞大而复杂的主题,数据库管理也是如此,安全性更是如此。开发一个安全的数据库应用程序并不容易——即使是经验丰富的开发人员也可能会遇到问题。

  • 在stackoverflow上很多回答并没有帮助。当人们编写使用动态SQL和参数插值的问题时,往往缺乏建议改用参数化语句的响应。有几次我提出使用预处理语句的建议被反驳,通常是因为被认为存在无法接受的性能开销。我严重怀疑这些提问者中大多数人都不会因为几毫秒额外的预处理时间对他们的应用程序产生灾难性的影响。


  • 10

    很好的问题。 答案更接近随机而非确定性,我将尝试用一个小例子来解释我的观点。

    网络上有很多参考建议我们在查询中使用参数或者使用带参数的存储过程以避免SQL注入(SQLi)。我将向您展示存储过程(例如)并不是防止SQL注入的魔法棒。责任仍然在程序员身上。

    考虑以下SQL Server存储过程,将从表“Users”中获取用户行:

    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 查询,我会使用参数串联的方式。你有什么建议吗? - TheProvost
    @TheProvost 这是一个好问题。考虑使用 sp_executesql:https://msdn.microsoft.com/zh-cn/library/ms188001.aspx - Tim
    @Tim 你好,Tim。我对动态SQL还不熟悉。sp_executesql和EXECUTE(@SqlQuery)有什么区别? - TheProvost
    2
    我认为这篇文章很好地解释了一个简单的例子:http://www.codeproject.com/Tips/586207/How-to-prevent-SQL-Injection-in-Stored-Procedures -- 但基本上,EXECUTE(@SqlQuery)无法防止SQL注入,然而sp_executesql(@SqlQuery, ..., ...)可以预防它。微软文章中的示例应该会有所帮助。 - Tim
    Tim已经有了解决方案TheProvost... ;) 你可以使用sp_executesql(@QUERY, @PARAMETERS, @VARS)...来处理动态SQL情况... ;) - Andreas Venieris

    7
    是的,使用预处理语句可以防止所有SQL注入攻击,至少在理论上是如此。实际上,参数化语句可能不是真正的预处理语句,例如,在PHP中,PDO 默认情况下会模拟它们,因此存在边缘情况的攻击

    如果您使用真正的预处理语句,一切都是安全的。当然,只要您不将不安全的SQL串联到查询中作为无法准备表名的反应。

    是的,为什么还有这么多成功的SQL注入攻击?只是因为一些开发者太愚蠢而不能使用参数化语句吗?

    是的,教育是主要问题,还有遗留代码库。许多教程使用转义字符,但不幸的是,它们不能很容易地从Web中删除。

    1
    这个链接回答实际上与预处理语句无关。 - Your Common Sense
    1
    @YourCommonSense:这涉及到参数化查询,根据所使用的驱动程序不同,它们可能并非真正的预处理语句,而是模拟出来的。了解这一点非常重要,并且与之密切相关... - kelunik
    1
    同一页中的另一个答案有一个非常好的评论:“如果您的所有查询都是参数化的,那么您也可以防止二次注入。一级注入是忘记用户数据是不可信的。二级注入是忘记数据库数据是不可信的(因为它最初来自用户)。” - Rodrigo
    @kelunik 这个链接的答案也不是关于参数化查询的,而是关于一个本质上伪造参数化查询的库。参数化查询是指将带有独立参数值的查询发送到服务器的一种方式。 - Panagiotis Kanavos
    1
    @PanagiotisKanavos:我非常了解那个答案的内容。这只是一个例子(也是相当普遍的例子),你使用的参数化查询可能实际上并没有被实现为预处理语句... - kelunik

    3
    我在编程中避免使用绝对值;总有例外情况。我强烈推荐使用存储过程和命令对象。我的大部分背景是SQL Server,但我也会偶尔尝试使用MySQL。存储过程有许多优点,包括缓存查询计划;是的,这也可以通过参数和内联SQL来实现,但这会打开更多注入攻击的可能性,并且无法帮助解决关注点分离问题。对我来说,更容易保护数据库,因为我的应用程序通常只能执行所述存储过程的权限。没有直接的表/视图访问权限,注入任何东西就更加困难了。如果应用程序用户受到损害,只有执行预定义内容的权限。以上是我的个人意见。

    这与问题有什么关系?您将如何调用和传递参数到存储过程?使用字符串拼接还是使用参数化查询?此外 - 如果有人在存储过程内部使用字符串拼接来创建“动态”查询怎么办?仅仅因为它是一个存储过程并不意味着它更安全。 - Panagiotis Kanavos
    通常我使用命令对象,也通过设计避免运行“动态查询”。 - Derek

    2

    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注入攻击,但并非绝对安全。


    2
    首先回答您的第一个问题:是的,据我所知,通过使用参数化查询,将不再可能进行SQL注入攻击。至于您接下来的问题,我不确定,只能就原因给出我的意见:
    我认为“只需”通过连接一些不同部分(甚至依赖于某些逻辑检查)的值来编写SQL查询字符串更容易。 它只是创建查询并执行它。 另一个优点是,您可以打印(echo,输出或其他方式)SQL查询字符串,然后使用此字符串进行手动查询到数据库引擎。
    当使用预处理语句时,您总是需要多做一步: 您必须构建查询(当然包括参数) 您必须在服务器上准备查询 您必须将参数绑定到要用于查询的实际值 您必须执行查询。
    这是有点更多的工作(而且不那么直观),特别是对于一些“快速而肮脏”的工作,这些工作经常被证明是非常持久的...
    最好的问候, Box

    2
    我不会说“愚蠢”。 我认为教程是问题所在。大多数SQL教程、书籍等都使用内联值来解释SQL,根本没有提到绑定参数。从这些教程中学习的人没有机会正确学习。

    2
    这还不够。为什么人们不使用框架或一些ORM呢?为什么他们不用一些愚蠢的测试工具来测试“愚蠢注入”呢?因为有时候老板给的工资不高,或者他给你X金额的项目,你需要从一个项目跑到另一个项目来赚点钱。你必须越来越快。程序员压力大,被逼迫过度,所以代码虽然能工作,但写得糟糕。 - jedi

    2

    因为大部分代码并非从安全性的角度编写,而且管理层往往选择增加功能(尤其是那些可视化、有销售价值的功能)而不是保障安全/稳定性/可靠性(这是更难以推销的)。只有在安全问题成为问题时,安全才会成为一个问题。


    2
    可以使用参数化语句来防止所有SQL注入吗?
    是的,只要您的数据库驱动程序提供每个可能的SQL文本的站位符。大多数准备好的语句驱动程序都没有。比如说,您永远不会找到一个用于字段名或值数组的站位符。这将使开发人员退回手工定制查询,使用连接和手动格式化,并带有预测结果。
    这就是为什么我为PHP制作了Mysql包装器,支持动态添加到查询中的大多数文字,包括数组和标识符。
    如果是这样,为什么仍然存在许多成功的SQL注入攻击呢?仅仅因为一些开发人员太愚蠢而无法使用参数化语句吗?
    正如您所看到的,在现实中,即使您不是愚蠢的人,也不可能将所有查询参数化。

    如果你的所有查询都是参数化的(无论是来自用户数据还是数据库数据),那么就像这里最受欢迎的评论所说,你似乎受到了保护:https://dev59.com/xHVC5IYBdhLWcg3w9GLM#134138 - Rodrigo
    我之所以征询您的意见,只是因为您似乎足够理性。如果我在其他地方读到的内容属实,我不认为您的方法会是最佳选择。无论如何,如果您能改进“使用常规工具无法将所有查询参数化”,我会非常感激。 - Rodrigo
    我开始阅读你的回答,直到我看到了“SQL字面量”这个概念。这个想法似乎不太对(它似乎过于繁琐)。如果参数化查询确实可以避免PHP中的注入攻击(我还在研究中),那么我的下一步就是避免JavaScript注入攻击。然后,我会回来研究你的解决方案。另外,我正在使用Postgres,也许你的解决方案只适用于MySQL? - Rodrigo
    好的,现在我再次阅读它,并不认为“参数化所有查询都是不可能的”是一种改进。在MySQL中不可能吗?在PostgreSQL中也不可能吗?为什么?我的php脚本外面有任何查询吗?在哪里?你所说的标识符是否指尝试从$_POST数组中剥离保留字?对我来说,这似乎不是正确的方法(当然,我可能直觉上错了)。此外,“你试过绑定过它吗?”我没明白你要绑定什么? - Rodrigo
    在网上找到这个信息没有我想象的那么容易。如果可能,请附上参考资料。 - Rodrigo

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接