1. 程式人生 > >PreparedStatement是如何防止SQL註入的?

PreparedStatement是如何防止SQL註入的?

jdb new hex exceptio 復制 orm mes 形式 loaddata

為什麽在Java中PreparedStatement能夠有效防止SQL註入?這可能是每個Java程序員思考過的問題。

首先我們來看下直觀的現象(註:需要提前打開mysql的SQL文日誌)

  1. 不使用PreparedStatement的set方法設置參數(效果跟Statement相似,相當於執行靜態SQL)

String param = "‘test‘ or 1=1";
String sql = "select file from file where name = " + param; // 拼接SQL參數
PreparedStatement preparedStatement = connection.prepareStatement(sql);

ResultSet resultSet = preparedStatement.executeQuery();
System.out.println(resultSet.next());
輸出結果為true,DB中執行的SQL為

-- 永真條件1=1成為了查詢條件的一部分,可以返回所有數據,造成了SQL註入問題
select file from file where name = ‘test‘ or 1=1

  1. 使用PreparedStatement的set方法設置參數

復制代碼
String param = "‘test‘ or 1=1";
String sql = "select file from file where name = ?";

PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, param);
ResultSet resultSet = preparedStatement.executeQuery();
System.out.println(resultSet.next());
復制代碼
輸出結果為false,DB中執行的SQL為

select file from file where name = ‘\‘test\‘ or 1=1‘
我們可以看到輸出的SQL文是把整個參數用引號包起來,並把引號作為轉義字符,從而避免了參數也作為條件的一部分

接下來我們分析下源碼(以mysql驅動實現為例)

打開java.sql.PreparedStatement通用接口,看到如下註釋,了解到PreparedStatement就是為了提高statement(包括SQL,存儲過程等)執行的效率。

An object that represents a precompiled SQL statement.
A SQL statement is precompiled and stored in a PreparedStatement object.
This object can then be used to efficiently execute this statement multiple times.
那麽,什麽是所謂的“precompiled SQL statement”呢?

回答這個問題之前需要先了解下一個SQL文在DB中執行的具體步驟:

Convert given SQL query into DB format -- 將SQL語句轉化為DB形式(語法樹結構)
Check for syntax -- 檢查語法
Check for semantics -- 檢查語義
Prepare execution plan -- 準備執行計劃(也是優化的過程,這個步驟比較重要,關系到你SQL文的效率,準備在後續文章介紹)
Set the run-time values into the query -- 設置運行時的參數
Run the query and fetch the output -- 執行查詢並取得結果
而所謂的“precompiled SQL statement”,就是同樣的SQL文(包括不同參數的),1-4步驟只在第一次執行,所以大大提高了執行效率(特別是對於需要重復執行同一SQL的)

言歸正傳,回到source中,我們重點關註一下setString方法(因為其它設置參數的方法諸如setInt,setDouble之類,編譯器會檢查參數類型,已經避免了SQL註入。)

查看mysql中實現PreparedStatement接口的類com.mysql.jdbc.PreparedStatement中的setString方法(部分代碼)

復制代碼
public void setString(int parameterIndex, String x) throws SQLException {
synchronized (checkClosed().getConnectionMutex()) {
// if the passed string is null, then set this column to null
if (x == null) {
setNull(parameterIndex, Types.CHAR);
} else {
checkClosed();

            int stringLength = x.length();

            if (this.connection.isNoBackslashEscapesSet()) {
                // Scan for any nasty chars
                // 判斷是否需要轉義處理(比如包含引號,換行等字符)
                boolean needsHexEscape = isEscapeNeededForString(x, stringLength); 
                // 如果不需要轉義,則在兩邊加上單引號
                if (!needsHexEscape) {
                    byte[] parameterAsBytes = null;

                    StringBuilder quotedString = new StringBuilder(x.length() + 2);
                    quotedString.append(‘\‘‘);
                    quotedString.append(x);
                    quotedString.append(‘\‘‘);

                    ...
                } else {
                    ...
            }

            String parameterAsString = x;
            boolean needsQuoted = true;
            // 如果需要轉義,則做轉義處理
            if (this.isLoadDataQuery || isEscapeNeededForString(x, stringLength)) {
            ...

復制代碼
從上面加紅色註釋的可以明白為什麽參數會被單引號包裹,並且類似單引號之類的特殊字符會被轉義處理,就是因為這些代碼的控制避免了SQL註入。

這裏只對SQL註入相關的代碼進行解讀,如果在setString前後輸出預處理語句(preparedStatement.toString()),會發現如下輸出

Before bind: com.mysql.jdbc.JDBC42PreparedStatement@b1a58a3: select file from file where name = NOT SPECIFIED
After bind: com.mysql.jdbc.JDBC42PreparedStatement@b1a58a3: select file from file where name = ‘\‘test\‘ or 1=1‘
編程中建議大家使用PrepareStatement + Bind-variable的方式避免SQL註入

大家有什麽其它的看法,歡迎留下評論!

PreparedStatement是如何防止SQL註入的?