看以下三种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);
}
我们介绍了利用绑定变量、通配符和利用esapi三种方式对sql注入进行防御,我的建议是尽量使用绑定变量或者通配符的是形式进行防注入,安全性能都比较好,如果不得不使用字符串拼接的形式,则使用esapi进行sql过滤
最后给个esapi的maven支持形式
org.owasp.esapi
esapi
2.1.0.1
Q.E.D.