常见漏洞防御 之 防 SQL 注入的三种方式

看以下三种 SQL 语句


String sql1 = "select  from user where username = '"+username+"'and password ='"+password+"'";
String sql2 ="select
from user where username = :username and password = :password";
String sql3 = "select from user where username = ? and password = ?";

 


推荐写法是 sql2、sql3 使用的别名或者通配符的形式进行传值,可以避免 SQL 注入,sql1 就有了常见的典型 SQL 注入问题,假设 username 随便传个值,比如 '张三',传入的 password 的值为  


 ' or 1 = 1' 则 拼接的 SQL 语句成了 


select  from user where username ='张三' and password = '' or 1 = 1''

很明显无论前面的 and 为真还是假,只要 or 后面为真,则就可使查询为 true,导致 SQL 注入 所以尽可能避免字符串拼接的形式进行 SQL 传值,但有些场景可能必须采用字符串拼接的形式,比如 Mybatis 在按要求排序时:


order by #{order} #{sort}

用 #{} 的形式取值就相当于字符串拼接,但是因为排序 DESC、AESC 这种是 SQL 关键字,不能进行转义,所以就必须采用字符串拼接的形式了


如果采用这种形式还要避免 SQL 注入的话,就需要对传入的值进行 SQL 过滤,来避免 SQL 注入


那么 OWASP 提供了一个防御 sql 注入的 Esapi 包,这个包中的 encodeForSQL 方法能对 sql 注入进行很好的防御。


// 防止 Oracle 注入
ESAPI.encoder().encodeForSQL(new OracleCodec(),queryparam)
// 防止 mysql 注入
ESAPI.encoder().encodeForSQL(new MySQLCodec(Mode.STANDARD),queryparam) //Mode.STANDARK 为标准的防注入方式,mysql 一般用使用的是这个方式
// 防止 DB2 注入
ESAPI.encoder().encodeForSQL(new DB2Codec(),queryparam) 

使用不同的数据库,使用的过滤方法不同


下面我们就用MySQL为例字分析 encodeForSQL 函数做了什么防御。具体函数过程就不跟踪了,直接分析最后调用了哪个方法。根据代码可知最后调用的是 encodeCharacter 方法。


 


public String encodeCharacter(char[] immune, Character c ) {
char ch = c.charValue();
// check for immune characters
if (containsCharacter( ch, immune) ) {
return ""+ch;
}
// check for alphanumeric characters
String hex = Codec.getHexForNonAlphanumeric(ch);
if (hex == null) {
return ""+ch;
}
switch(mode) {
case ANSI: return encodeCharacterANSI(c);
case STANDARD: return encodeCharacterMySQL(c);
}
return null;
}

 


上述方法中 containsCharacter 函数是不进行验证的字符串白名单,Codec.getHexForNonAlphanumeric 函数查找字符传中是否有 16 进制,没有返回空值。


 


而 encodeCharacterANSI 和 encodeCharacterMySQL 才是防御的重点,我们看一下这两个函数的不同,如果选择的我们选择是 Mode.ANSi 模式,则字符串则进入下面的函数,可以看到这个函数对单撇号和双撇号进行了转义。


 


private String encodeCharacterANSI(Character c) {
if (c == '\'')
return "\'\'";
if (c == '\"')
return "";
return ""+c;
}

 


如果选择的是 Mode.STANDARD 模式,则字符串则进入下面的函数,可以看到这个函数对单撇号和双撇号、百分号、反斜线等更多的符号进行了转换,所以使用时推荐使用标准模式。


 


private String encodeCharacterMySQL(Character c) {
char ch = c.charValue();
if (ch == 0x00) return "\0";
if (ch == 0x08) return "\b";
if (ch == 0x09) return "\t";
if (ch == 0x0a) return "\n";
if (ch == 0x0d) return "\r";
if (ch == 0x1a) return "\Z";
if (ch == 0x22) return "\\"";
if (ch == 0x25) return "\%";
if (ch == 0x27) return "\'";
if (ch == 0x5c) return "\\";
if (ch == 0x5f) return "\_";
return "\" + c;
}

 


写个单元测试:


@org.junit.Test
public void testESAPI() {
String username = "zhangsan";
String password = "' or 1=1'";
String sql1 = "select from user where username = '"+username+"'and password ='"+password+"'";
// String sql2 ="select
from user where username = :username and password = :password";
// String sql3 = "select from user where username = ? and password = ?";
System.out.println("过滤前:"+sql1);
username = ESAPI.encoder().encodeForSQL(new MySQLCodec((Mode.STANDARD)), username);
password = ESAPI.encoder().encodeForSQL(new MySQLCodec((Mode.STANDARD)), password);
sql1 = "select
from user where username = '"+username+"'and password ='"+password+"'";
System.out.println("过滤后:"+sql1);
}

 


SQL注入


 

我们介绍了利用绑定变量、通配符和利用 esapi 三种方式对 sql 注入进行防御,我的建议是尽量使用绑定变量或者通配符的是形式进行防注入,安全性能都比较好,如果不得不使用字符串拼接的形式,则使用 esapi 进行 sql 过滤


最后给个 esapi 的 maven 支持形式


    <dependency>
<groupId>org.owasp.esapi</groupId>
<artifactId>esapi</artifactId>
<version>2.1.0.1</version>
</dependency>