搜尋此網誌

2010年10月8日 星期五

[技術分享] 用了參數化查詢就可以對 SQL Injection 高枕無憂? NO!

sql-injection在上個月的一篇文章中,我們看到 WhiteHat Security 對於避免 SQL Injection 的危害提出了 10 個手法。其中第 7 個手法,使用”參數化的查詢 (在 Java 的程式語言中主要是透過 PreparedStatement 來提供) 並避免使用動態式的查詢”對許多人來說應該算是老生常談了。事實上,程式內未採用參數化查詢大多不是因為技術上的考量,因為除了極少數的情況 (通常與效能有關) 外,參數化查詢都是可以順利運作的。在無法採用參數化查詢的原因中,最常見的情況就是使用了預儲程序 (Stored Procedure)。預儲程序如果小心使用,對於 SQL Injection 其實也有一定的防護能力。但是預儲程序畢竟不像參數化查詢那般嚴謹,所以提供這些功能的程式碼無法事先做太多的防範措施,大多數防護的責任還是落在系統開發者自行開發的程式上。而參數化查詢因為會嚴格限制每個參數的型別,所以提供此一功能的程式就可以做好萬全的準備,讓系統開發者在撰寫自己的程式時不需太過擔心。所以除非被限制必須使用預儲程序來存取資料,否則我們就應該採用參數化查詢來存取資料。

參數化查詢與預儲程序之間的取捨,往往還有許多其他非技術的因素,但是這並不是我這篇文章的重點,所以這個問題就在此打住。當我們確實採用了參數化的查詢後,是否就可以對 SQL Injection 高枕無憂了呢?很可惜的是,答案依舊是否定的。如果我們仔細審視第一段的藍色文字,可以發現後半段是避免使用動態式的查詢。而前、後段文字使用”並”加以連結,所以一旦使用者違反了後半段文字的限制,即使使用參數化查詢依舊會遭受 SQL Injection 的攻擊。因為這點對於部分的系統開發者來說或許並不是那麼熟悉,所以接下來我將以實際的例子 (Java 語法) 來加以說明。我假設我們擁有一個稱為 users 的資料表,內含 name 與 password 兩個欄位,分別表示使用者的姓名與密碼。而在此資料表中有一個使用者的姓名與密碼分別為 ‘cyril wang’ 與 ’1234’。

1. 這是 PreparedStatement 的標準用法,也就是利用 setXXX 的方式 (程式碼第 9, 10 行) 來明確指定資料的型別。因為使用者輸入 (參見備註1) 的帳號與密碼相對應,所以此程式執行完後可以取得該使用者的ID。

備註1:雖然在所有的程式範例中, userInputForUsername 與 userInputForPassword 兩個變數的值是固定的,但是在一般程式中此兩變數的值通常來自於一般使用者 (或駭客) 的輸入。

  1: Class.forName("com.mysql.jdbc.Driver");
  2: Connection c =
  3:     DriverManager.getConnection(url, account, password);
  4: 
  5: String userInputForUsername = "cyril wang";
  6: String userInputForPassword = "1234";
  7: String sql = "SELECT * FROM users WHERE name=? AND password=?";
  8: PreparedStatement stmt = c.prepareStatement(sql);
  9: stmt.setString(1, userInputForUsername);
 10: stmt.setString(2, userInputForPassword);
 11: ResultSet rs = stmt.executeQuery();
 12: if (rs.next()) {
 13: 	System.out.println("Your ID is " + rs.getInt(1));
 14: } else {
 15: 	System.out.println("Get out!");
 16: }


 



2. 當使用者 (或駭客) 嘗試輸入 SQL Injection 的攻擊字串,PreparedStatement 的實作會將字串內容加以過濾。因為帳號與密碼無法對應,因此無法取得使用者的 ID。



  1: Class.forName("com.mysql.jdbc.Driver");
  2: Connection c =
  3:     DriverManager.getConnection(url, account, password);
  4: 
  5: String userInputForUsername = "cyril wang' OR 1=1--'";
  6: String userInputForPassword = "aaaa";
  7: String sql = "SELECT * FROM users WHERE name=? and password=?";
  8: PreparedStatement stmt = c.prepareStatement(sql);
  9: stmt.setString(1, userInputForUsername);
 10: stmt.setString(2, userInputForPassword);
 11: ResultSet rs = stmt.executeQuery();
 12: if (rs.next()) {
 13: 	System.out.println("Your ID is " + rs.getInt(1));
 14: } else {
 15: 	System.out.println("Get out!");
 16: }


 



3. 仔細看程式碼第 5 行到第 11 行的寫法,這種常見於一般 SQL 查詢的用法對 PreparedStatement 來說也是合法的,而這種用法就稱為動態式查詢。因為使用者輸入的對應的帳號與密碼,因此可以取得 ID。



  1: Class.forName("com.mysql.jdbc.Driver");
  2: Connection c = 
  3:     DriverManager.getConnection(url, account, password);
  4: 
  5: String userInputForUsername = "cyril wang";
  6: String userInputForPassword = "1234";
  7: String sql = "SELECT * FROM users WHERE name = '" +
  8: 	         userInputForUsername +
  9: 	         "' AND password ='" +
 10: 	         userInputForPassword +
 11: 	         "'";
 12: PreparedStatement stmt = c.prepareStatement(sql);
 13: ResultSet rs = stmt.executeQuery();
 14: if (rs.next()) {
 15: 	System.out.println("Your ID is " + rs.getInt(1));
 16: } else {
 17: 	System.out.println("Get out!");
 18: }


 



4. 使用者 (或駭客) 再次嘗試輸入 SQL Injection 攻擊字串,而這將是一次成功的攻擊。儘管沒有輸入對應的密碼,使用者 (或駭客) 依舊取得 ID。



  1: Class.forName("com.mysql.jdbc.Driver");
  2: Connection c = 
  3:     DriverManager.getConnection(url, account, password);
  4: 
  5: String userInputForUsername = "cyril wang' OR 1=1--'";
  6: String userInputForPassword = "aaaa";
  7: String sql = "SELECT * FROM users WHERE name = '" +
  8: 	         userInputForUsername +
  9: 	         "' AND password ='" +
 10: 	         userInputForPassword +
 11: 	         "'";
 12: PreparedStatement stmt = c.prepareStatement(sql);
 13: ResultSet rs = stmt.executeQuery();
 14: if (rs.next()) {
 15: 	System.out.println("Your ID is " + rs.getInt(1));
 16: } else {
 17: 	System.out.println("Get out!");
 18: }


 



從上面這些簡單的範例程式中,我們可以看到如果在參數化查詢指令中使用了動態式的查詢,那麼程式依舊無法避免 SQL Injection 的問題,也因此我們應該盡力避免使用動態式的查詢。如果動態式的查詢確實有其必要性,做好輸入的檢查 (Input Validation & Sanitization) 就是不可避免的手法。其實以安全的角度來看,不管是不是使用參數化查詢,都應該做好輸入的檢查動作才是。



以目前常見的兩大網站安全問題來看,SQL Injection 的問題與解決之道相對比 Cross-Site Scripting 單純多了,因此我們實在沒有理由讓 SQL Injection 一直肆虐下去。對於已經完成或開發中的程式,採用自動化的白箱工具可以有效地找出所有有問題的程式碼,以避免花費大量時間進行人工搜尋。而透過對系統開發人員的教育訓練,則可以讓 SQL Injection 的問題防範於未然,此時白箱工具則可由原先找出問題的角色轉換為確保安全品質與稽核的角色。正確的觀念 (從單位主管到系統開發人員) 再加上合適的工具,SQL Injection 的避免並沒有想像中的那麼遙不可及。



備註2: 在多層次防禦的概念下,除了使用參數化查詢外,其他手法還是有其必要性,而前述 WhiteHat Security 的文章可作為參考。但是千萬切記勿落入”這就是全部可用的手法”的誤解中,以免因為疏忽而產生危害。

About