SQL经常和程序代码一块儿使用。咱们一般所说的SQL动态查询,是指将程序中的变量和基本SQL语句拼接成一个完整的查询语句。html
string sql = SELECT * FROM Person WHERE Id = $Id
咱们指望$Id是一个整型,所以当数据库接收到这个请求时,$Id的值就是查询语句的一部分。git
SQL动态查询是有效利用数据库很天然的方法。当你使用程序内的变量来指定如何进行查询时,就是将SQL做为链接程序和数据库的桥梁。程序和数据库之间经过这种方式进行“对话”。程序员
然而,要让程序按照你想要的方式执行并不难,难的是让程序变得安全,不执行你不想让它执行的操做。但软件在收到SQL注入攻击时,一般都没法保证安全。web
当往SQL查询的字符串中插入别的内容,而这些被插入的内容以你不但愿的方式修改了查询语句的语法时,SQL注入就成功了。如,对于上面所写的SQL语句,$Id的值变为"123; DELETE FROM Person"。那么最终的查询语句会变成这样:sql
SELECT * ROM Person WHERE Id = 123; DELETE FORM Person
若是你的程序真的执行了这样一行SQL语句,那么悲剧了,Person表里的数据就彻底被清空了。数据库
一、对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语句:oracle
UPDATE Account SET Password = 123456 WHERE AccountId = 123 OR TRUE
Account表中,全部用户的密码都被改为了123456。
有数不尽的方法选择一个恶意的字符串来改变SQL语句的行为。它只受制于攻击者的想象力和你保护SQL语句的能力。
二、寻找治愈良方
有不少文章,声称某一种技术室对抗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('123456') 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 = 1; //设置参数值
大多数开发人员都推荐这个方案,由于你不须要对动态内容进行转义,或者担忧有缺陷的转义函数。
事实上,查询参数这个方法的确是对付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','123456' --SQL注入,无需帐号行数不返回0 EXECUTE SelectAccount 'a'' or 1=1 --','';
返回结果:
事实上几乎全部的数据库应用程序都动态地构建SQL语句。若是你使用拼接字符串的形式或者将变量插入到字符串中的方法来构建哪怕一句SQL语句,那这一句查询语句就会让应用程序暴露在SQL注入攻击的威胁之下。
没有哪种技术能使SQL代码变得安全,你应该学习下面所描述的全部技术,并在合理的地方使用它们。
一、过滤输入内容
你应该将全部不合法的字符从用户输入中剔除掉,而不是纠结因而否有些输入包含了有危险的内容。也就是说,若是你须要一个整数,那就只使用输入中的整数部分。根据你所使用的开发语言不一样,方法也不尽相同。
二、参数化动态内容
若是查询中的变化部分是一些简单的类型,你应该使用查询参数将其和SQL表达式分离。
参数化动态内容以后,一个参数只能被替换成一个值。若是你是在RDMBS解析完SQL语句以后才插入这个参数值,没有那种SQL注入的攻击可以改变一个参数化了的查询的语法结构。即便攻击者尝试使用带有恶意的参数值,注入123 OR TRUE,RDBMS会将这个字符串当成一个完整的值插入。最坏的状况下,这个查询没办法返回任何记录,它不会返回错误的行。
三、给动态输入的值加引号
查询参数一般来讲是最好的解决方案,但在有些很特殊的状况下,参数的占位符会致使查询优化器没法正确选择使用哪一个索引来进行优化。要规避参数查询对索引的影响,直接将变量内容插入到SQL语句中会是更好的方法,不要去理会查询参数。一旦你决定这么作了,就必定要当心地引用字符串。你须要确信你插入的字符串是通过严格测试、不会带有安全隐患的。
四、将用户与代码隔离
查询参数和转移字符能帮助你将字符串类型的值插入到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)