MyBatis 中使用 sqlMap 進行 sql 查詢時,經常需要動態傳遞參數,例如sql 如下:
select * from student where uid=#{uid} AND student_name='${studentName}'
在動態 SQL 解析階段, #{ } 和 ${ } 會有不同的表現:
#{ } 解析為一個 JDBC 預編譯語句(prepared statement)的參數標記符占位符 ?。
上面的例子就被解析為
...... uid=?
${ } 僅僅為一個純碎的 string 替換,在mybatis的動態 SQL 解析階段將會進行變量替換。比如:傳入的studentName參數是“張三”
...... student_name='張三'
最后解析完成的完整語句就是
select * from student where uid= ? AND student_name='張三'
然后把解析好的語句在通過JDBC執行。
所以因為在${ } 在預編譯之前已經被變量替換了,這會存在 sql 注入問題。比如:傳入的studentName參數是“1' OR '1'='1”
在mybatis中解析完成后變成了
select * from student where uid= ? AND student_name='1' OR '1'='1'
這樣就查詢出所有的數據了。
源碼查看:
在執行mybtis的查詢是,會執行到PreparedStatementHandler#query,查看生成的PreparedStatement
如果傳入參數傳入的studentName參數是“1' OR '1'='1”
所以就出現注入的情況了。
如果我們把${studentName}換成#{studentName},在傳入“1' OR '1'='1”呢
在mybatis解析出的結果
在數據庫的執行日志中查詢執行的sql語句
發現我們傳入的“'”被轉義了,
所以通過預編譯,用占位符的方式傳值可以把一些特殊的字符進行轉義,這樣可以防止一些sql注入。
其原因就是:采用了JDBC的PreparedStatement,就會將sql語句:“select * from student where uid= ? AND student_name= ?” 預先編譯好,也就是SQL引擎會預先進行語法分析,產生語法樹,生成執行計劃,也就是說,后面你輸入的參數,無論你輸入的是什么,都不會影響該SQL語句的語法結構了,因為語法分析已經完成了,而語法分析主要是分析sql命令,比如select,from,where,and,or,order by等等。所以即使你后面輸入了這些sql命令,也不會被當成sql命令來執行了,因為這些SQL命令的執行,必須先得通過語法分析,生成執行計劃,既然語法分析已經完成,已經預編譯過了,那么后面輸入的參數,是絕對不可能作為SQL命令來執行的,只會被當做字符串字面值參數。所以的sql語句預編譯可以防御SQL注入。而且在多次執行同一個SQL時,能夠提高效率。原因是SQL已編譯好,再次執行時無需再編譯。
默認使用PreparedStatement是不能執行預編譯的,這需要在url中給出useServerPrepStmts=true參數(MySQL Server 4.1之前的版本是不支持預編譯的,而Connector/J在5.0.5以后的版本,默認是沒有開啟預編譯功能的)。
例如:jdbc:mysql://localhost:3306/test?useServerPrepStmts=true
這樣才能保證mysql驅動會先把SQL語句發送給服務器進行預編譯,然后在執行executeQuery()時只是把參數發送給服務器。