PreparedStatement如何避免或预防SQL注入?

157

我知道使用 PreparedStatements 可以避免 SQL 注入攻击。它是如何做到的呢?最终使用 PreparedStatements 构建的查询语句会是字符串形式还是其他形式?


3
技术上来说,JDBC规范并不要求没有SQL注入漏洞。我不知道有哪些驱动程序受到了影响。 - Tom Hawtin - tackline
4
@Jayesh 我建议您将博客内容添加为答案。大多数答案只是在讲述动态SQL查询生成和预处理语句之间的区别,而没有解决为什么预处理语句更好的问题,而这正是您的博客所涉及的。 - Pavan Manjunath
1
已添加为答案,希望能有所帮助。 - Jayesh
10个回答

226

考虑两种完成同一件事情的方式:

PreparedStatement stmt = conn.createStatement("INSERT INTO students VALUES('" + user + "')");
stmt.execute();

或者

PreparedStatement stmt = conn.prepareStatement("INSERT INTO student VALUES(?)");
stmt.setString(1, user);
stmt.execute();
如果“user”来自用户输入,并且用户输入是
Robert'); DROP TABLE students; --

在第一种情况下,你将会遭受损失。而在第二种情况下,你将会安全,并且“小博比·特别表”(Little Bobby Tables)会被注册到你的学校。


8
如果我理解正确,第二个例子中将被执行的查询实际上是:INSERT INTO student VALUES("Robert'); DROP TABLE students; --"),或者类似于这样的内容。这是真的吗? - Max
22
不,首先,您会得到这个陈述。在第二个示例中,它会将“Robert'); DROP TABLE students;--”插入用户表中。 - Paul Tomblin
4
我在第二个例子中的意思是,字符串*Robert'); DROP TABLE students; --*将被保存到学生表中的字段中。我写错了什么吗?;) - Max
8
抱歉,嵌套引用是我尽量避免使用的,因为容易造成混淆。这就是为什么我喜欢带参数的PreparedStatements。 - Paul Tomblin
68
小博比·泰布尔斯。XD很好的参考。 - Alkanshel
显示剩余2条评论

165
为了理解PreparedStatement如何防止SQL注入,我们需要了解SQL查询执行的阶段。
1. 编译阶段。 2. 执行阶段。
每当SQL服务器引擎接收到一个查询时,它必须经过以下阶段。

Query Execution Phases


  1. 解析和规范化阶段: 在这个阶段,查询语句会被检查语法和语义。它会检查查询中引用的表格和列是否存在。 还有很多其他任务需要完成,但我们不详细讨论。

  2. 编译阶段: 在这个阶段,查询中使用的关键字(如select、from、where等)会被转换成机器可理解的格式。 这是解释查询并决定要采取的相应操作的阶段。还有很多其他任务需要完成,但我们不详细讨论。

  3. 查询优化计划: 在这个阶段,创建决策树来找到执行查询的方法。 它找出查询可以执行的方式数量以及每种执行查询方式的成本。 它选择最佳计划来执行查询。

  4. 缓存: 在查询优化计划中选择的最佳计划被存储在缓存中,以便下次遇到相同的查询时,不必再经过第1、2和3个阶段。 当下次查询进来时,将直接在缓存中进行检查并从中提取以执行。

  5. 执行阶段: 在这个阶段,提供的查询被执行并将数据作为ResultSet对象返回给用户。

PreparedStatement API在上述步骤中的行为

  1. PreparedStatements不是完整的SQL查询,而是包含占位符,在运行时由实际提供的用户数据替换。

  2. 无论何时传递包含占位符的任何PreparedStatment给SQL Server引擎,它都会经过以下阶段:

    1. 解析和规范化阶段
    2. 编译阶段
    3. 查询优化计划
    4. 缓存(带有占位符的编译查询存储在缓存中。)

UPDATE user set username=? and password=? WHERE id=?

  1. 上述查询将被解析、编译并进行特殊处理的占位符优化,并被缓存。 此时的查询已经被编译并转换成机器可理解的格式。 因此我们可以说存储在缓存中的查询是预编译的,只需要用用户提供的数据替换占位符。

  2. 现在,在运行时当用户提供的数据到来时,从缓存中挑选预编译的查询并用用户提供的数据替换占位符。

PrepareStatementWorking

请记住,当占位符被用户数据替换后,最终查询不会再次编译/解释,SQL Server引擎将用户数据视为纯数据而不是需要重新解析或编译的SQL;这就是PreparedStatement的优点。
如果查询不必再次经过编译阶段,则替换占位符的任何数据都被视为纯数据,并且对SQL Server引擎没有意义,它直接执行查询。
注意:在解析阶段之后的编译阶段才能理解/解释查询结构并赋予其有意义的行为。在PreparedStatement的情况下,查询仅编译一次,并且缓存已编译的查询一直用于替换用户数据和执行。
由于PreparedStatement的一次性编译特性,它可以免受SQL注入攻击。
您可以在此处获得详细的说明和示例: https://javabypatel.blogspot.com/2015/09/how-prepared-statement-in-java-prevents-sql-injection.html

7
好的解释。 - Dheeraj Joshi
12
如何运行的最全面的答案,字面意思如下。 - jouell
2
非常有帮助。感谢详细的解释。 - Unknown

88

SQL注入问题在于用户输入被用作SQL语句的一部分。通过使用预编译语句,您可以强制将用户输入处理为参数的内容(而不是SQL命令的一部分)。

但如果您不使用用户输入作为预编译语句的参数,而是通过拼接字符串构建SQL命令,则即使使用预编译语句,您仍然会容易受到SQL注入攻击


1
当然可以,但你仍然可以硬编码一些或所有的参数。 - tangens
18
如果您不将用户输入作为预处理语句的参数,而是通过拼接字符串构建SQL命令,即使使用预处理语句,您仍然容易受到SQL注入攻击。 - david blaine
6
就翻译而言,FWIW准备语句不是JDBC的专属功能,而是SQL的通用功能。你可以在SQL控制台中准备并执行预处理语句。PreparedStatement只是从JDBC中支持它们。 - beldaz

29

在PreparedStatement中使用的SQL语句是由驱动程序预编译的。从那时起,参数被作为字面值而不是可执行的SQL部分发送到驱动程序;因此,不能使用参数注入任何SQL。 PreparedStatement的另一个有益的副作用(预编译+仅发送参数)是,在多次运行语句时,即使参数的值不同(假设驱动程序支持PreparedStatement),也可以提高性能,因为驱动程序不必每次参数更改时执行SQL解析和编译。


不一定要这样实现,我相信通常情况下也不是这样的。 - Tom Hawtin - tackline
5
实际上,SQL 通常在数据库上进行预编译。也就是说,会在数据库上准备一个执行计划。当您执行查询时,该计划将使用这些参数进行执行。额外的好处是,同一语句可以使用不同的参数执行,而无需每次都让查询处理器编译新的计划。 - beldaz

4

我猜它会是一个字符串。但输入参数将被发送到数据库,并在创建实际的SQL语句之前应用适当的强制转换/转换。

举个例子,它可能会尝试并查看CAST / Conversion是否有效。
如果有效,它可以将其转化为最终语句。

   SELECT * From MyTable WHERE param = CAST('10; DROP TABLE Other' AS varchar(30))

尝试一个接受数字参数的SQL语句示例。
现在,尝试传递一个字符串变量(其内容可以作为数字参数接受)。它会引发任何错误吗?

现在,尝试传递一个字符串变量(其内容无法作为数字参数接受)。看看会发生什么?


3

SQL注入:当用户有机会输入可能成为SQL语句一部分的内容时

例如:

String query = “INSERT INTO students VALUES(‘” + user + “‘)”

当用户将“Robert’); DROP TABLE students; –”作为输入时,会导致SQL注入

准备好的语句如何防止这种情况发生?

String query = “INSERT INTO students VALUES(‘” + “:name” + “‘)”

parameters.addValue(“name”, user);

=> 当用户再次输入“Robert’); DROP TABLE students; –”时,输入字符串在驱动程序上被预编译为字面值,我想它可能会被转换为:

CAST(‘Robert’); DROP TABLE students; –‘ AS varchar(30))

因此,最终字符串将被文字插入到表中。

http://blog.linguiming.com/index.php/2018/01/10/why-prepared-statement-avoids-sql-injection/


1
如果我没记错的话,CAST(‘Robert’); DROP TABLE students; –‘ AS varchar(30)) 中的部分 CAST(‘Robert’); 会出问题,然后如果是这种情况,它会继续删除表。它确实阻止了注入,因此我认为这个例子并不足以解释这种情况。 - Héctor Álvarez

3

预编译语句更加安全。它会将参数转换为指定类型。

例如,stmt.setString(1, user); 将会把 user 参数转换成一个字符串。

假设参数包含一个 SQL 字符串并含有可执行命令:使用预编译语句将不允许这样做。

它会添加元字符(也称自动转换)到其中。

这使其更加安全。


1

PreparedStatement 单独使用并不能帮助您,如果仍然在连接字符串,则容易受到以下攻击:

  • 调用睡眠函数以使所有数据库连接都处于忙碌状态,从而使应用程序无法使用
  • 从 DB 中提取敏感数据
  • 绕过用户身份验证

不仅 SQL,即使是 JPQL 或 HQL 也可能会受到影响,如果您没有使用绑定参数。

总之,在构建 SQL 语句时,永远不要使用字符串连接。使用专用的 API,如 JPA Criteria API。


1
感谢您指出使用参数绑定的重要性,而不仅仅是PreparedStatement。然而,您的回复似乎暗示着使用专用API才能保护免受SQL注入攻击。由于这并非事实,并且使用带有参数绑定的PreparedStatement也可以起到作用,您是否愿意重新表述一下? - Wild Pottok

1

PreparedStatement:

1)预编译和数据库端缓存SQL语句可实现更快的执行速度,并能在批处理中重复使用同一SQL语句。

2)通过内置引号和其他特殊字符的转义,自动防止SQL注入攻击。请注意,这需要您使用任何PreparedStatement setXxx()方法来设置值。


-3
在预处理语句中,用户被强制将数据作为参数输入。如果用户输入一些易受攻击的语句,如DROP TABLE或SELECT * FROM USERS,则数据不会受到影响,因为这些语句将被视为SQL语句的参数。

与所选答案相同,但精度较低。 - Julien Maret

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