prepared statements 如何帮助我们防止 SQL injection 攻击?
维基百科说:
准备好的语句对 SQL 注入具有弹性,因为稍后使用不同协议传输的参数值不需要正确转义。如果原始语句模板不是来自外部输入,则不会发生 SQL 注入。
我不能很好地看到原因。什么是简单的英语和一些例子的简单解释?
这个想法很简单——查询和数据分别发送到数据库服务器。就这样。
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);
所以它不能改变我们的程序并造成任何伤害。很简单——不是吗?
我唯一要补充的是,在每本手册中总是省略:
准备好的语句只能保护数据文字,但不能与任何其他查询部分一起使用。
因此,一旦我们必须添加动态的标识符 - 一个例如,字段名称 - 准备好的语句对我们没有帮助。我有 explained the matter recently,所以我不会重复自己。
这是用于设置示例的 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 子句中添加相当于“或真”的内容来操纵输入,从而破坏查询,从而返回所有内容,包括本应隐藏的关于 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 语句缓存命中,而绑定示例始终运行相同的语句)。
这是一个避免这种注入的绑定示例:
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=?
PREPARE
创建一个已解析的固定命名语句(即,无论输入如何,该语句都不会再更改),而 EXECUTE
将运行绑定参数的命名语句。由于 PREPARE
只有会话持续时间,它看起来确实是出于性能原因,而不是用于防止通过 psql 脚本进行注入。对于 psql 访问,可以授予存储过程的权限并绑定 procs 内的参数。
基本上,使用准备好的语句,来自潜在黑客的数据被视为数据 - 它不可能与您的应用程序 SQL 混合和/或被解释为 SQL(当传入的数据直接放入您的应用程序 SQL)。
这是因为准备好的语句首先“准备”SQL 查询以找到一个有效的查询计划,然后发送可能来自表单的实际值 - 那时实际执行查询。
更多精彩信息在这里:
Prepared statements and SQL Injection
我通读了答案,仍然觉得有必要强调阐明准备好的陈述本质的关键点。考虑两种查询涉及用户输入的数据库的方法:
天真的方法
一种是将用户输入与一些部分 SQL 字符串连接起来以生成 SQL 语句。在这种情况下,用户可以嵌入恶意 SQL 命令,然后将其发送到数据库执行。
String SQLString = "SELECT * FROM CUSTOMERS WHERE NAME='"+userInput+"'"
例如,恶意用户输入可能导致 SQLString
等于 "SELECT * FROM CUSTOMERS WHERE NAME='James';DROP TABLE CUSTOMERS;'
由于恶意用户,SQLString
包含 2 条语句,其中第 2 条 ("DROP TABLE CUSTOMERS"
) 会造成伤害。
准备好的报表
在这种情况下,由于查询与数据,用户输入永远不会被视为 SQL 语句,因此永远不会执行。正是由于这个原因,任何注入的恶意 SQL 代码都不会造成任何伤害。因此,在上述情况下永远不会执行 "DROP TABLE CUSTOMERS"
。
简而言之,通过用户输入引入的带有预处理语句的恶意代码将不会被执行!
当您创建准备好的语句并将其发送到 DBMS 时,它会存储为 SQL 查询以供执行。
您稍后将数据绑定到查询,以便 DBMS 使用该数据作为查询参数来执行(参数化)。 DBMS 不会使用您绑定的数据作为已编译 SQL 查询的补充;这只是数据。
这意味着使用预准备语句执行 SQL 注入基本上是不可能的。准备好的语句的本质及其与 DBMS 的关系阻止了这一点。
在 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 注入......
关键字是need not be correctly escaped
。这意味着您无需担心人们会尝试使用破折号、撇号、引号等...
这一切都为您处理。
ResultSet rs = statement.executeQuery("select * from foo where value = " + httpRequest.getParameter("filter");
让我们假设您在 Servlet 中拥有它是对的。如果一个恶意的人为“过滤器”传递了一个错误的值,你可能会破解你的数据库。
简单的例子:
"select * from myTable where name = " + condition;
如果用户输入是:
'123'; delete from myTable; commit;
查询将像这样执行:
select * from myTable where name = '123'; delete from myTable; commit;
根本原因 #1 - 分隔符问题
Sql 注入是可能的,因为我们使用引号来分隔字符串并且也是字符串的一部分,因此有时无法解释它们。如果我们有不能在字符串数据中使用的分隔符,sql 注入永远不会发生。解决定界符问题就消除了sql注入问题。结构查询就是这样做的。
根本原因#2 - 人性,人是狡猾的,有些狡猾的人是恶意的,所有人都会犯错误
sql注入的另一个根本原因是人性。包括程序员在内的人都会犯错误。当您在结构化查询中出错时,它不会使您的系统容易受到 sql 注入的影响。如果不使用结构化查询,错误可能会产生 sql 注入漏洞。
结构化查询如何解决 SQL 注入的根本原因
结构化查询通过将 sql 命令放在一个语句中并将数据放在单独的编程语句中来解决分隔符问题。编程语句创建所需的分离。
结构化查询有助于防止人为错误造成严重的安全漏洞。关于人为错误,使用结构查询时不会发生sql注入。有一些方法可以防止不涉及结构化查询的 sql 注入,但是这种方法中的正常人为错误通常会导致至少一些暴露于 sql 注入。结构化查询不会受到 sql 注入的影响。你几乎可以用结构化查询犯世界上所有的错误,就像任何其他编程一样,但你犯的任何错误都不能变成被 sql 注入接管的 ssstem。这就是为什么人们喜欢说这是防止sql注入的正确方法。
所以,你有它,sql注入的原因和使它们在使用时不可能的性质结构化查询。
$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);
。他们不会做同样的事情吗?