预准备语句如何防范SQL注入攻击?

How can prepared statements protect from SQL injection attacks?

提问人:Aan 提问时间:11/25/2011 最后编辑:Aan 更新时间:5/12/2022 访问量:190040

问:

准备好的语句如何帮助我们防止SQL注入攻击?

维基百科 说:

预准备语句对 SQL 注入具有弹性,因为 参数值,稍后使用不同的 协议,不需要正确转义。如果原始声明 template 不是从外部输入派生的,SQL 注入不能 发生。

我看不清原因。用简单的英语和一些例子进行简单的解释是什么?

SQL 安全性 sql 注入 prepared-statement

评论


答:

3赞 Matthew Cox 11/25/2011 #1

关键短语是 。这意味着您无需担心人们试图添加破折号、撇号、引号等......need not be correctly escaped

这一切都为您处理。

2赞 MeBigFatGuy 11/25/2011 #2
ResultSet rs = statement.executeQuery("select * from foo where value = " + httpRequest.getParameter("filter");

让我们假设你在 Servlet 中拥有它,对吧。如果恶意人员为“过滤器”传递了错误的值,您可能会入侵您的数据库。

31赞 Glenn 11/25/2011 #3

下面是用于设置示例的 SQL 语句:

CREATE TABLE employee(name varchar, paymentType varchar, amount bigint);

INSERT INTO employee VALUES('Aaron', 'salary', 100);
INSERT INTO employee VALUES('Aaron', 'bonus', 50);
INSERT INTO employee VALUES('Bob', 'salary', 50);
INSERT INTO employee VALUES('Bob', 'bonus', 0);

Inject 类容易受到 SQL 注入的攻击。查询将与用户输入动态粘贴在一起。查询的目的是显示有关 Bob 的信息。工资或奖金,基于用户输入。但是恶意用户通过在 where 子句中附加等效的“或 true”来操纵输入,从而破坏查询,以便返回所有内容,包括应该隐藏的有关 Aaron 的信息。

import java.sql.*;

public class Inject {

    public static void main(String[] args) throws SQLException {

        String url = "jdbc:postgresql://localhost/postgres?user=user&password=pwd";
        Connection conn = DriverManager.getConnection(url);

        Statement stmt = conn.createStatement();
        String sql = "SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType='" + args[0] + "'";
        System.out.println(sql);
        ResultSet rs = stmt.executeQuery(sql);

        while (rs.next()) {
            System.out.println(rs.getString("paymentType") + " " + rs.getLong("amount"));
        }
    }
}

运行此程序,第一种情况是正常使用,第二种情况是恶意注入:

c:\temp>java Inject salary
SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType='salary'
salary 50

c:\temp>java Inject "salary' OR 'a'!='b"
SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType='salary' OR 'a'!='b'
salary 100
bonus 50
salary 50
bonus 0

不应使用用户输入的字符串串联来构建 SQL 语句。它不仅容易受到注入的影响,而且对服务器也有缓存影响(语句会更改,因此不太可能得到 SQL 语句缓存命中,而绑定示例始终运行相同的语句)。

下面是一个 Binding 示例,用于避免此类注入:

import java.sql.*;

public class Bind {

    public static void main(String[] args) throws SQLException {

        String url = "jdbc:postgresql://localhost/postgres?user=postgres&password=postgres";
        Connection conn = DriverManager.getConnection(url);

        String sql = "SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType=?";
        System.out.println(sql);

        PreparedStatement stmt = conn.prepareStatement(sql);
        stmt.setString(1, args[0]);

        ResultSet rs = stmt.executeQuery();

        while (rs.next()) {
            System.out.println(rs.getString("paymentType") + " " + rs.getLong("amount"));
        }
    }
}

使用与上一个示例相同的输入运行此命令显示恶意代码不起作用,因为没有与该字符串匹配的 paymentType:

c:\temp>java Bind salary
SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType=?
salary 50

c:\temp>java Bind "salary' OR 'a'!='b"
SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType=?

评论

0赞 Celeritas 11/13/2015
使用连接到数据库的程序中的预准备语句是否与使用作为数据库一部分的预准备语句具有相同的效果?例如,Postgres 有自己的准备好的语句,使用它会阻止 SQL 注入吗?postgresql.org/docs/9.2/static/sql-prepare.html
0赞 Glenn 11/13/2015
@Celeritas我对Postgresql没有明确的答案。查看文档,效果似乎是一样的。 创建一个已解析的固定命名语句(即,无论输入如何,该语句都不会再更改),同时将运行绑定参数的命名语句。由于只有会话持续时间,因此看起来它确实是出于性能原因,而不是为了防止通过 psql 脚本进行注入。对于 psql 访问,可以授予对存储过程的权限,并在进程中绑定参数。PREPAREEXECUTEPREPARE
0赞 Krishna Pandey 1/29/2019
@Celeritas我在 x86_64 上使用 PostgreSQL 11.1 尝试了上述代码,并且上面的 SQLi 示例有效。
396赞 Your Common Sense 11/25/2011 #4

这个想法非常简单 - 查询和数据分别发送到数据库服务器。
就这样。

SQL注入问题的根源在于代码和数据的混合。

事实上,我们的 SQL 查询是一个合法的程序。 我们正在动态创建这样一个程序,即时添加一些数据。因此,数据可能会干扰程序代码,甚至改变它,正如每个 SQL 注入示例所示(PHP/Mysql 中的所有示例):

$expected_data = 1;
$query = "SELECT * FROM users where id=$expected_data";

将生成常规查询

SELECT * FROM users where id=1

虽然这段代码

$spoiled_data = "1; DROP TABLE users;"
$query        = "SELECT * FROM users where id=$spoiled_data";

将产生恶意序列

SELECT * FROM users where id=1; DROP TABLE users;

它之所以有效,是因为我们将数据直接添加到程序主体中,并且它成为程序的一部分,因此数据可能会改变程序,并且根据传递的数据,我们将删除常规输出或表。users

虽然在准备好的声明的情况下,我们不会改变我们的程序,但它仍然完好无损
这就是重点。

我们首先向服务器发送一个程序

$db->prepare("SELECT * FROM users where id=?");

其中数据被称为参数或占位符的变量替换。

请注意,将完全相同的查询发送到服务器,其中没有任何数据!然后,我们将数据与第二个请求一起发送,基本上与查询本身分开:

$db->execute($data);

所以它不能改变我们的程序并造成任何伤害。
很简单 - 不是吗?

我唯一需要补充的是,每本手册中总是省略的:

预准备语句只能保护数据文本,但不能与任何其他查询部件一起使用。 因此,一旦我们必须添加动态标识符(例如字段名称),预准备语句就无能为力了。
我最近已经解释了这件事,所以我不会重复自己。

评论

2赞 pinepain 6/18/2013
“例如,默认情况下,PDO 不使用预处理语句” - 这并不完全正确,因为 PDO 仅针对不支持此类功能的驱动程序模拟预处理语句。
4赞 cHao 8/18/2013
@zaq178miami:“PDO 仅针对不支持该功能的驱动程序模拟准备好的语句” - 并不完全正确。MySQL支持预处理语句已经有一段时间了。PDO 驱动程序也有。但是,默认情况下,MySQL查询仍然是由PDO准备的,上次我检查过。
21赞 Juha Untinen 9/18/2014
-> 与 -> -> 相比有什么不同。他们不会做同样的事情吗?$spoiled_data = "1; DROP TABLE users;"$query = "SELECT * FROM users where id=$spoiled_data";$db->prepare("SELECT * FROM users where id=?");$data = "1; DROP TABLE users;"$db->execute($data);
31赞 Soley 9/7/2015
@Juha Untinen 数据可以是任何东西。它不会解析数据。那是 DATA,而不是命令。因此,即使$data包含 sql 命令,也不会执行。此外,如果 id 是数字,则字符串内容将生成报告或值为零。
11赞 srinivas 4/10/2021
@JuhaUntinen我认为当使用准备好的语句执行查询时,查询将像这样执行: SELECT * FROM users where id='1;DROP TABLE users“,因此永远无法在数据库中找到此值,并且不会返回任何内容。哦,哎呀!!所以它也把我们从SQL注入中拯救出来!!!!!干杯
4赞 lloydom 11/25/2011 #5

SQL Server 中,使用预准备语句绝对是防注入的,因为输入参数不会构成查询。这意味着执行的查询不是动态查询。 SQL注入漏洞语句示例。

string sqlquery = "select * from table where username='" + inputusername +"' and password='" + pass + "'";

现在,如果 inoutusername 变量中的值类似于 a' 或 1=1 --,则此查询现在变为:

select * from table where username='a' or 1=1 -- and password=asda

其余的在 之后被注释,因此它永远不会被执行和绕过,因为使用准备好的语句示例如下。--

Sqlcommand command = new sqlcommand("select * from table where username = @userinput and password=@pass");
command.Parameters.Add(new SqlParameter("@userinput", 100));
command.Parameters.Add(new SqlParameter("@pass", 100));
command.prepare();

因此,实际上您不能发送另一个参数,从而避免了 SQL 注入......

17赞 Jose 4/21/2012 #6

基本上,对于准备好的语句,来自潜在黑客的数据被视为数据 - 并且它不可能与应用程序 SQL 混合和/或被解释为 SQL(当传入的数据直接放入应用程序 SQL 时,可能会发生这种情况)。

这是因为预准备语句首先“准备”SQL 查询以找到有效的查询计划,然后发送可能来自表单的实际值 - 此时查询已实际执行。

更多精彩信息在这里:

预准备语句和 SQL 注入

6赞 wulfgarpro 7/13/2016 #7

创建预准备语句并将其发送到 DBMS 时,该语句将存储为 SQL 查询以供执行。

稍后将数据绑定到查询,以便 DBMS 使用该数据作为执行(参数化)的查询参数。DBMS 不会使用绑定的数据作为已编译 SQL 查询的补充;它只是数据。

这意味着从根本上不可能使用预准备语句执行 SQL 注入。预准备语句的本质及其与DBMS的关系阻止了这一点。

2赞 DanAllen 4/8/2017 #8

根本原因 #1 - 分隔符问题

SQL注入是可能的,因为我们使用引号来分隔字符串,也是字符串的一部分,因此有时无法解释它们。如果我们的分隔符不能在字符串数据中使用,那么 sql 注入就不会发生。解决分隔符问题消除了 sql 注入问题。结构查询就是这样做的。

根本原因 #2 - 人性,人是狡猾的,有些狡猾的人是恶意的,所有人都会犯错

sql注入的另一个根本原因是人性。人们,包括程序员,都会犯错误。当您在结构化查询上犯错误时,它不会使您的系统容易受到 sql 注入的攻击。如果不使用结构化查询,错误可能会生成 sql 注入漏洞。

结构化查询如何解决 SQL 注入的根本原因

结构化查询通过将 sql 命令放在一个语句中并将数据放在单独的编程语句中来解决分隔符问题。编程语句创建所需的分离。

结构化查询有助于防止人为错误造成严重的安全漏洞。关于人类犯错,当使用结构查询时,SQL注入不会发生。有一些方法可以防止不涉及结构化查询的 sql 注入,但这种方法中的正常人为错误通常至少会导致一些 sql 注入暴露。结构化查询对 sql 注入具有故障保护功能。你几乎可以用结构化查询犯世界上所有的错误,就像任何其他编程一样,但你可以犯的任何错误都不能变成一个被sql注入接管的ssstem。这就是为什么人们喜欢说这是防止 sql 注入的正确方法。

所以,你有它,sql注入的原因和性质结构化查询,使它们在使用时变得不可能。

13赞 N.Vegeta 12/27/2017 #9

我通读了答案,仍然觉得有必要强调阐明预备发言本质的关键点。考虑两种方法来查询涉及用户输入的数据库:

幼稚的方法

一种将用户输入与一些部分 SQL 字符串连接起来以生成 SQL 语句。在这种情况下,用户可以嵌入恶意 SQL 命令,然后将这些命令发送到数据库执行。

String SQLString = "SELECT * FROM CUSTOMERS WHERE NAME='"+userInput+"'"

例如,恶意用户输入可能导致SQLString"SELECT * FROM CUSTOMERS WHERE NAME='James';DROP TABLE CUSTOMERS;'

由于恶意用户,包含 2 条语句,其中第 2 条()会造成伤害。SQLString"DROP TABLE CUSTOMERS"

准备好的发言

在这种情况下,由于查询和数据的分离,用户输入永远不会被视为 SQL 语句,因此永远不会执行。正是出于这个原因,注入的任何恶意 SQL 代码都不会造成伤害。因此,在上述情况下永远不会执行。"DROP TABLE CUSTOMERS"

简而言之,使用准备好的语句,通过用户输入引入的恶意代码将不会被执行!

评论

2赞 Your Common Sense 12/27/2017
真?公认的答案并没有确切地说明这一点吗?
0赞 N.Vegeta 1/5/2018
@Your常识:公认的答案充满了很多有价值的信息,但它让我想知道数据和查询分离的实现细节需要什么。而专注于恶意注入的数据(如果有的话)永远不会被执行这一点,一针见血。
1赞 Your Common Sense 1/5/2018
您的答案中提供了哪些没有提供的“实施细节”?
0赞 N.Vegeta 1/5/2018
如果你试着看看我来自哪里,你会意识到我的观点如下:短暂地希望看到实现细节,源于需要了解恶意用户输入不会造成任何伤害的明确原因。与其说是需要查看实现细节,不如说是需要查看实现细节。这就是为什么意识到实现细节是这样的,在任何时候都不会执行恶意输入的 SQL,将消息发送回家。你的回答回答了这个问题,如何(按照要求)?,但我想其他人(像我一样)会对为什么的简洁回答感到满意?
4赞 N.Vegeta 1/5/2018
说数据与查询分开发送,并且程序保持不变,仍然没有显示如何准确防止伤害。是因为查询部分在执行前经过筛选了吗?是因为查询从未执行过吗?这正是引发人们想要了解引擎盖下到底发生了什么的思考过程。我相信我的回答回答了这一点,并得到了赞成票,然后是反对票(我猜来自你),希望你能明白为什么这对其他人有用。
2赞 sin906 5/12/2022 #10

简单的例子:

  "select * from myTable where name = " + condition;

如果用户输入是:

  '123'; delete from myTable; commit;

查询将按如下方式执行:

  select * from myTable where name = '123'; delete from myTable; commit;