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()時只是把引數傳送給伺服器。