一、目标:编写SQL动态查询
SQL常常和程序代码一起使用。我们通常所说的SQL动态查询,是指将程序中的变量和基本SQL语句拼接成一个完整的查询语句。
string sql = SELECT * FROM Person WHERE Id = $Id
我们期望$Id是一个整型,因此当数据库接收到这个请求时,$Id的值就是查询语句的一部分。
SQL动态查询是有效利用数据库很自然的方法。当你使用程序内的变量来指定如何进行查询时,就是将SQL作为连接程序和数据库的桥梁。程序和数据库之间通过这种方式进行“对话”。
然而,要让程序按照你想要的方式执行并不难,难的是让程序变得安全,不执行你不想让它执行的操作。但软件在收到SQL注入攻击时,通常都无法保证安全。
二、反模式:将未经验证的输入作为代码执行
当往SQL查询的字符串中插入别的内容,而这些被插入的内容以你不希望的方式修改了查询语句的语法时,SQL注入就成功了。如,对于上面所写的SQL语句,$Id的值变为"123; DELETE FROM Person"。那么最终的查询语句会变成这样:
SELECT * ROM Person WHERE Id = 123; DELETE FORM Person
如果你的程序真的执行了这样一行SQL语句,那么悲剧了,Person表里的数据就完全被清空了。
1、对Web安全的严重威胁
当攻击者能够使用SQL注入操控你的SQL查询时,它就变成了一个巨大的威胁。假设你的数据库中修改密码的SQL语句是这样写的:
UPDATE Account SET Password = SHA2('$password') WHERE AccountId = 123;
那么聪明的攻击者会猜测你请求参数对应在SQL语句中的作用,并且精心选择每个参数对应的值:
http://www.xxx.com/setpassword?password=123456&userid=123 OR TRUE
如果你的程序真的被攻击者所绕过了,那么你的数据库将会执行如下SQL语句:
UPDATE Account SET Password = 123456 WHERE AccountId = 123 OR TRUE
Account表中,所有用户的密码都被改成了123456。
有数不尽的方法选择一个恶意的字符串来改变SQL语句的行为。它只受制于攻击者的想象力和你保护SQL语句的能力。
2、寻找治愈良方
有很多文章,声称某一种技术室对抗SQL注入的万能药。而事实上,这些技术都被证明无法阻挡所有类型的SQL注入。因此你需要在不同的情况下,将所有这些技术组合起来使用。
(1).转义
防止SQL语句包含任何不匹配的引号是最古老的方法,就是对所有的引号字符进行转义操作,使它们不至于成为字符串的结束符。在标准SQL语句中,可以使用两个连续的单引号来表示一个单引号字符:
SELECT * FROM Person WHERE Name = 'O''Hare'
大多数数据库还支持使用反斜杠对单引号进行转义操作:
SELECT * FROM Person WHERE Name = 'O\'Hare'
这么做的原理是,将应用程序中的数据插入到SQL语句之前就进行转换,大多数SQL的编程接口都会提供一个简便的函数来做这个操作。
这样做了之后,所有的字符串都会被包上引号,如果对上面修改账号的SQL语句这样操作的话,SQL语句会变成这样:
UPDATE Account SET Password = SHA2('')
WHERE AccountId = '123 OR TRUE'
在SQL中,是没有办法让一个数值列直接和一个带有数字的字符串进行比较的,不管是哪种数据库,这都不可以。在标准SQL中,将字符串转换为数字时,必须明确使用CASE()函数,因此上面的SQL语句只是报个错误,还不至于全部用户账号的密码都被修改了。
(2).查询参数
一个经常被认为是防止SQL语句注入的万能解决方案是使用"参数化查询",不同于在SQL语句中插入动态内容,查询参数的做法是在准备查询语句的时候,在对应参数的地方使用参数占位符。随后,在执行这个预先准备好的查询时提供一个参数。
cmd.CommandText = "Update Person Set Name = 'Ado.net' WHERE Id = @Id"; //设置操作语句
cmd.Parameters.Add("@Id", SqlDbType.Int); //添加参数
cmd.Parameters["@Id"].Value = ; //设置参数值
大多数开发人员都推荐这个方案,因为你不需要对动态内容进行转义,或者担心有缺陷的转义函数。
事实上,查询参数这个方法的确是对付SQL注入一个强劲有效的解决方案。但这并不是一个通用的方案,因为查询参数总是被视为一个字面值。
多个值的列表不可以当成单一参数:
cmd.CommandText = "Update Person Set Name = 'Ado.net' WHERE Id IN (@Id)"; //设置操作语句
cmd.Parameters.Add("@Id", SqlDbType.String); //添加参数
cmd.Parameters["@Id"].Value = "1,2,3"; //设置参数值
这个做法会导致数据库认为传入的是一个包含数字和逗号的字符串,处理过程将和一系列整数作为参数进行查询并不一样。
真正生成到数据库中执行的SQL语句为:
Update Person Set Name = 'Ado.net' WHERE Id IN ('1,2,3')
表名无法作为参数:
cmd.CommandText = "SELECT * FROM @Table; //设置操作语句
cmd.Parameters.Add("@Table", SqlDbType.String); //添加参数
cmd.Parameters["@Table"].Value = "Person"; //设置参数值
这么做是想将一个字符串插入表名所在的位置,但只会得到一个语法错误的提示。
真正生成到数据库执行的SQL语句如下:
SELECT * FROM 'Person'
列名无法作为参数:
cmd.CommandText = "SELECT * FROM Person ORDER BY @Column; //设置操作语句
cmd.Parameters.Add("@Column", SqlDbType.String); //添加参数
cmd.Parameters["@Column"].Value = "Id"; //设置参数值
真正生成到数据库执行的SQL语句如下:
SELECT * FROM Person ORDER BY 'Id'
SQL关键字不能作为参数:
cmd.CommandText = "SELECT * FROM Person ORDER BY Id @Sort; //设置操作语句
cmd.Parameters.Add("@Sort", SqlDbType.String); //添加参数
cmd.Parameters["@Sort"].Value = "DESC"; //设置参数值
参数将被当做字面字符串插入而非SQL关键字。在这个例子中,会返回语法错误的提示:
SELECT * FROM Person ORDER BY Id 'DESC'
(3).存储过程
存储过程,是很多程序员生成可以抵御SQL注入攻击的方法。通常来说,存储过程包含固定的SQL语句,这些语句是在定义这个存储过程的时候被解析的。
然而,存储过程也是使用SQL动态查询的,无法绝对保证完全杜绝SQL注入。不过存储过程的确是有强大的防止SQL注入的作用。不过,如果你依然是在存储过程中拼接SQL语句,存储过程也一样能够注入。
假设你的存储过程如下(这在比较复杂一些的操作时经常出现的):
CREATE PROC SelectAccount
@name varchar(50),
@password varchar(50)
AS
DECLARE @sql varchar(1000);
SET @sql = 'SELECT * FROM Account WHERE Name = ''' + @name + ''' AND Password = ''' + @password + ''''
EXEC (@sql)
如果在存储过程当中拼接SQL语句,那么注入方式如下:
--正常登录
EXECUTE SelectAccount 'admin',''
--SQL注入,无需账号行数不返回0
EXECUTE SelectAccount 'a'' or 1=1 --','';
返回结果:
事实上几乎所有的数据库应用程序都动态地构建SQL语句。如果你使用拼接字符串的形式或者将变量插入到字符串中的方法来构建哪怕一句SQL语句,那这一句查询语句就会让应用程序暴露在SQL注入攻击的威胁之下。
三、解决方案:不相信任何人
没有哪一种技术能使SQL代码变得安全,你应该学习下面所描述的所有技术,并在合理的地方使用它们。
1、过滤输入内容
你应该将所有不合法的字符从用户输入中剔除掉,而不是纠结于是否有些输入包含了有危险的内容。也就是说,如果你需要一个整数,那就只使用输入中的整数部分。根据你所使用的开发语言不同,方法也不尽相同。
2、参数化动态内容
如果查询中的变化部分是一些简单的类型,你应该使用查询参数将其和SQL表达式分离。
参数化动态内容之后,一个参数只能被替换成一个值。如果你是在RDMBS解析完SQL语句之后才插入这个参数值,没有那种SQL注入的攻击能够改变一个参数化了的查询的语法结构。即使攻击者尝试使用带有恶意的参数值,注入123 OR TRUE,RDBMS会将这个字符串当成一个完整的值插入。最坏的情况下,这个查询没办法返回任何记录,它不会返回错误的行。
3、给动态输入的值加引号
查询参数通常来说是最好的解决方案,但在有些很特殊的情况下,参数的占位符会导致查询优化器无法正确选择使用哪个索引来进行优化。要规避参数查询对索引的影响,直接将变量内容插入到SQL语句中会是更好的方法,不要去理会查询参数。一旦你决定这么做了,就一定要小心地引用字符串。你需要确信你插入的字符串是经过严格测试、不会带有安全隐患的。
4、将用户与代码隔离
查询参数和转移字符能帮助你将字符串类型的值插入到SQL表达式中,但这些技术在需要插入表/列名或者SQL关键字的时候不起作用。你需要另一项技术来使得这些部分也能动态化。
对应的解决方案是,将请求参数作为索引值去查找预先定义好的值,然后用这些预先定义好的值来组织SQL查询语句。
(1)、预定义值
如在一个数据字典中存储如下值:
up => "ASC" ,down => "DESC"
(2)、定义默认值
定义一个默认值,当用户选择的值不在数据字典中时,使用默认值。
比如当用户输入的值不是up也不为down,那么就使用ASC。这样的字符串就是安全的了。
下面给出一个简单的SQL注入过滤函数:
public string NoSqlHack(string Inner)
{
if (!string.IsNullOrEmpty(Inner))
{
//特殊的字符
Inner = Inner.Replace("<", "");
Inner = Inner.Replace(">", "");
Inner = Inner.Replace("*", "");
Inner = Inner.Replace("-", "");
Inner = Inner.Replace("?", "");
Inner = Inner.Replace("'", "''");
Inner = Inner.Replace(",", "");
Inner = Inner.Replace("/", "");
Inner = Inner.Replace(";", "");
Inner = Inner.Replace("*/", "");
Inner = Inner.Replace("\r\n", "");
Inner = Inner.Replace(" ", ""); return Inner;
}
else
{
return string.Empty;
}
}
另外,后端一定要验证,以防止用户POST请求。
SQL注入绕过某些字符过滤:
1,避免使用被阻止的字符,即不使用这些字符仍然达到攻击目的。
A,如果注入一个数字数据字段,就不需要使用单引号。
B,输入注释符号被阻止使用,我们可以设计注入的数据,既不破坏周围的查询语法。
比如, http://www.xxx.net/article.asp?id=1' 这里存在注入,过滤了注释符合,我们可以输入 http://www.xxx.net/article.asp?id=1' or 'a'='a
目的其实很简单,就是把后面的单引号给闭合掉。
C,在一个MSSQL注入中注入批量查询的时候,不必使用分号分隔符。
只要纠正所有批量查询的语法,无论你是否使用分号,查询的解析器依然能正确的去解释它们的。
2,避免使用简单确认
一些输入确认机制使用一个简单的黑名单,组织或删除任何出现在这个名单中的数据,比如防注入程序。
这一般要看这个机制是否做的足够的好了,黑名单是否足够能确保安全。如果只是简单的黑名单,那也有机会突破的。
A,如果select关键词被阻止或删除
我们可以输入:
SeLeCt 注意大小写
selselectect 还记得ewebeditor是怎么过滤asp的么?
%53%45%4c%45%43%54 URL编码
%2553%2545%254c%2545%2543%2554 对上面的每个%后加了一个25
3,使用SQL注释符
A,使用注释来冒充注入的数据中的空格。
select/*alocne*/username,password/*alocne*/from/*alocne*/admin
/*alocne*/来冒充空格
B,使用注释来避开某些注入的确认过滤。
SEL/*alocne*/ECT username,password fr/*alocne*/om admin
4,处理被阻止的字符串
比如,程序阻止了admin,因为怕攻击者注入admin表单中的数据。
我们可以这样
A,oracle数据库: 'adm'||'in'
B,MSSQL数据库: 'adm'+'in'
C,MYSQL数据库: concat ('adm','in')
D,oracle中如果单引号被阻止了,还可以用chr函数
sleect password from admin where username = char(97) || chr(100) || chr(109) || chr(105) || chr(110)