資料隱碼攻擊(SQL Injection)之所以能夠發動,主要是因為資料庫存取程式利用來自於使用者所輸入的內容,做為建構SQL述句的一部份內容,使得最後所建構出來的SQL述句,違背了原設計者的預期,反而提供了別有用心的用途。

強制處理不該出現的輸入資料,阻斷執行
很多人想到的,便是過濾或改寫使用者提供的資料內容。例如,下列的SQL述句處理程式行:

strSQL = "SELECT * FROM users WHERE (name = '" + userName + "') and (pw = '"+ passWord +"');"

攻擊者可能會提供這樣子的輸入:

userName = "' OR '1'='1";
passWord = "' OR '1'='1";

來進行資料隱碼攻擊。所以,有些人想到,如何處理這有問題的輸入資料呢?他們想到的是,在這樣的例子中,正常使用者所提供的輸入資料,是不應該含有單引號的,而有心人士也正是因為透過了單引號的存在,才能引發攻擊。那是不是針對不預期、也不允許出現單引號的資料,進行過濾或處理,就能避掉資料隱碼攻擊呢?例如:

userName = userName. replace("\'", "\'\'");

也就是說,把userName中出現的單引號,一律取代成兩個連續的單引號。這種作法,等於是對單引號進行escape的動作。如此一來,倘若輸入的userName資料中含有單引號,就會改寫成連續的兩個單引號,也就使得所建構出來的SQL述句成為一個非法的SQL述句,最後無法執行。

這種escape的技巧看起來有用,不過其實還有盲點。怎麼說呢?像常見的MySQL、Sybase及Microsoft SQL Server,這幾種關聯式資料庫,都支援所謂CHAR()(而其他的資料庫伺服器則使用CHR())的函式,來允許在SQL述句中,直接寫下ASCII字元碼來表示想要使用的字元。

所以,假若有心人士所提供的資料中,並不是直接使用單引號字元,而是間接的改使用CHAR(39)(39是單引號的ASCII碼),那麼上述這種簡單escape掉單引號的方法就行不通,因為在輸入資料中並不會直接出現單引號,因此,也不會發生取代的動作,但最終所建構出來的字串,還是會出現單引號,而有心人士的目的依然能夠達到。

應透過程式庫來產生參數化的SQL述句
所以,只是很簡單地進行escape的動作,並不保證一定能發揮阻止的效用。想要建構安全的SQL述句,最理想的方式,還是不要透過在程式中串接、操作字串的方式來進行,而是利用程式庫中參數化建立SQL述句的方法來達成。

例如,在Java的JDBC程式庫中,提供了名為PreparedStatement的類別,來允許程式設計者建立參數化的SQL述句。例如,處理上例SQL述句的程式碼,就可以寫成如下的形式:

strSQL = "SELECT * FROM users WHERE (name = ?) and (pw = ?);"
PreparedStatement prepStmt = con.prepareStatement(strSQL);
prepStmt.setString(1, userName);
prepStmt.setString(2, password);
ResultSet rs = prepStmt.executeQuery();

來自於使用者輸入的資料userName及password,都不再透過字串的串接、操作來建構出SQL述句,而是做為一個SQL述句模板的參數傳入,由程式庫來負責底層建構SQL述句的工作。對PreparedStatement而言,JDBC的驅動程式內部,能自動的進行相關的escape動作,而且有助於防範資料隱碼攻擊。

而在.NET裡,在SQLCommand的Parameters屬性,也能提供類似的參數化SQL述句的作用。

總的來說,想要防範資料隱碼攻擊,在處理SQL述句的建構時,必須要留意幾項原則:(1)所有的查詢動作都應該要參數化,(2)而所有來自於使用者的輸入資料都必須放到參數中,(3)在建構動態的SQL述句時,絕不使用字串串接、操作的方式。

Stored Procedure的防範對策
除了對資料庫的查詢、更新,可能受到資料隱碼攻擊之外,當應用程式使用資料庫上的預儲程序(Stored Procedure)時,也有可能遭受到類似的攻擊。解決之道為何?同樣是針對要傳入預儲程序的資料內容,予以參數化。無論是Java或.NET的資料庫存取程式庫,都允許你參數化傳入預儲程序的資料內容。

在這邊你必須留意,使用諸如PreparedStatement之類的方式來進行資料存取,並不是預防資料隱碼攻擊的萬靈丹,也就是說,它之所以能夠起防禦的作用,是存在一個前提的──你沒有利用使用者所提供的輸入資料來動態組成SQL述句。如果前一個例子改寫成為:

strSQL = "SELECT * FROM users WHERE (name = '" + userName + "') and (pw = '"+ passWord +"');"
PreparedStatement prepStmt = con.prepareStatement(strSQL);
ResultSet rs = prepStmt.executeQuery();

雖然也運用了PreparedStatement,但是,因為SQL述句的內容依舊使用字串串接使用者輸入資料的方式,而不是將輸入資料做為參數提供給PreparedStatement,因此仍然無法抵擋資料隱碼攻擊。所以,在前段文字中所提到的三個原則,都必須遵守,使用參數化查詢才具備抵擋攻擊的能力。

作好防禦的基本檢查原則
除了運用資料庫存取程式庫所提供參數化SQL述句的方式之外,在防範資料隱碼攻擊上,你還可以有其他加強的空間。

資料隱碼攻擊先天上就是透過特意建造的資料來發動攻擊的,因此,本質上,我們可以針對資料進行諸多的檢核,來篩選出不允許的資料。

首先,你可以做的努力是限制資料的長度。例如,已經知道某個資料欄位必定是一個64位元的整數,那麼,在接收到資料後,程式便可以試著將它轉型成為整數,並且檢查其長度是否合理。如果輸入資料別有用心,那麼它極有可能無法滿足對資料的檢核,因而被過濾出來。

同樣的,像長度略長的字串型別輸入資料,在資料庫中也都有一定的長度限制,檢查其長度是否在限制之內,也有助於挑出有嫌疑的問題資料。

除此之外,一些格式更嚴謹的資料,像XML結構的資料,可以在處理之前,先利用XML結構的驗證器來驗證其結構是否正確,也能避掉一些被攻擊的機會。而不該出現HTML標籤的字串內容(例如不允許使用者輸入HTML內容的論壇文章),也可以檢查是否含有HTML標籤,而進一步加以阻止。

簡單來說,絕不相信使用者所提供的資料,是建構程式碼安全性的一個基礎原則,因為有心人士總是透過刻意造成的資料,來發動攻擊。因此,對使用者輸入資料,在效能不致於造成影響的情況下,進行最嚴格的檢核,往往是杜絕攻擊的關鍵方法。在資料庫存取上,每個資料欄位,都有它被賦予的意義,當然還有相對應的格式及值域。舉凡使用者ID、密碼、身份證字號、信用卡號碼、電子郵件位址、生日、費用、……等等,都可以明確定義出它們該有的格式,以及值的內容範圍。在接收到來自於使用者端的輸入資料後,便可以進行相對應的驗證及檢核,那麼,有心人士想要透過資料來發動攻擊,難度就會相對提高了。

作者簡介


Advertisement

更多 iThome相關內容