防堵SQL Injection

年前,一次全面性的SQL Injection攻擊,導致數千個網站受害,霎時間,讓所有人領教到看似方便的網路世界,也有其陰暗的一面。SQL Injection是一種著床於程式設計師的慣性及惰性上的害蟲,只要有程式設計師持續提供其養份

這是2008年我在MSDN寫的":LINQ - 對付SQL Injection 的"免費補洞策略" 的姊妹版, 原文連結早已躺平很久了, 我所能找到的就這一篇....or2

 

SQL Injection的起源

 

    數年前,一次全面性的SQL Injection攻擊,導致數千個網站受害,霎時間,讓所有人領教到看似方便的網路世界,也有其陰暗的一面。SQL Injection是一種著床於程式設計師的慣性及惰性上的害蟲,只要有程式設計師持續提供其養份,那麼它就能持續增長及變異。近日發生的新型態Injection攻擊:Mass SQL Injection就是一個鐵證,雖說是新型態的攻擊,但其仍然是循傳統的SQL Injection漏洞侵入,搭配上XSS(Cross-Site Scripting)及一個大量感染用的Hacker工具,轉眼間於近萬個網站中植入木馬,不過這並不是最可怕的部份,在筆者研究中發現,這個大量感染的Hacker工具原理相當簡單!拜現今程式開發工具的發達,一般初學者只要了解其原理,幾乎可在一天內寫出這種程式,然後透過搜尋引擎對大量網站進行掃蕩式的攻擊。如果你還不了解這個嚴重性,那麼只要想像著,改天瀏覽某一網站後,導致自己的個人資料外流、電腦被駭客當成跳板對其它網站攻擊、電腦中的檔案被破壞,最糟的是,只要SQL Injection存在一天,你就必須生活在這個陰影下!

 

透析SQL Injection

 

    SQL Injection的原理很簡單,它抓住了程式設計師常年來撰寫程式的慣性,利用URL參數或是網頁上的輸入文字框侵入,以一個簡單的ASP.NET頁面為例:

從ASP以來,程式設計師於處理此類登入介面時,通常會準備好一段SQL指令,然後將使用者輸入值與此段SQL指令組合,完成一段正確可向資料庫索取資料的SQL指令:

SqlCommand cmd = new SqlCommand("SELECT COUNT(*) FROM USERS WHERE USER_ID = '"
                + userName + "' AND PASSWORD = '" + password + "'", conn);

但設計師卻忽略了一件很重要的事,使用者可以在文字輸入框中輸入任何的字元,這包括了【'】,能輸入【'】有什麼問題嗎?問題很大,因為吃進了【'】,導致組裝式的SQL寫法產生了語法上的錯誤,較聰明的駭客還可以輕易的利用此錯誤來改變原來的SQL指令,假設駭客於密碼欄位上輸入以下字串:

' OR 1=1 --

那麼整串SQL指令將會變成:

SELECT COUNT(*) FROM USERS WHERE USER_ID = '' AND PASSWORD='' OR 1=1 --'

你發現了嗎?因為【'】的出現,導致駭客可於輸入框中輸入部份的SQL指令,然後我們的應用程式則忠實的將這些SQL指令送往資料庫執行,今天加的是【OR】,改天他可以加入【INSERT】甚至是【DROP TABLE】等破壞性的SQL指令。SQL Injection不只出現在文字輸入框,URL參數也是其入侵的漏洞之一,抓住了程式設計師常以URL參數做為網頁間參數傳遞的慣性,駭客們可以循同樣的方式來對網站進行SQL Injection攻擊:

if (!IsPostBack)
{
    SqlCommand cmd = new SqlCommand(
        "SELECT * FROM Customers WHERE CustomerID = '"+Request.QueryString["ID"]+"'", conn);
...............

Mass SQL Injection現身

if (!IsPostBack)
{
    SqlCommand cmd = new SqlCommand(
        "SELECT * FROM Customers WHERE CustomerID = '"+Request.QueryString["ID"]+"'", conn);

   在SQL Injection被發現的初期,我們大多認為其只會出現在文字輸入框,近日的Mass SQL Injection手法則推翻了這點,透過URL參數的方式,Mass SQL Injection成功的在數天內感染萬個網站。如果你還沒發現以URL參數手法完成的SQL Injection有多可怕,我建議你做一個動作,打開Google,然後輸入:

.asp?a=

不意外,你一定找到了許多以a做為參數的網站,呃!你是否有頭皮開始麻的感覺了?沒有?那麼請再輸入:

Details.aspx?id=

呼呼!我想你已經充份了解我的意思了。是的,這就是Mass SQL Injection的手法,利用一個簡單的程式,透過Google等搜尋網站來找尋標的,然後一一對其進行SQL Injection攻擊,與以往不同的是,Mass SQL Injection透過一串SQL指令對資料庫中所有的varchar、nvarchar寫入一段HTML碼,這段HTML碼中包含了一段引用某個網站的Script標籤。當你開啟被感染的網頁時,該網頁就預定的行為模式至資料庫取得想動態呈現的網頁內容,此時由於資料庫已被竄改過,取出的正是駭客們準備好的HTML碼,就這樣!SQL Injection與XSS完全緊密結合了。

 

非長久之治標之法

   

    我非常肯定,你一定知道避免SQL Injection的最佳方案就是使用參數化的SQL 指令,這是目前已知可以讓網站完全不受SQL Injection所擾的最佳辦法,如果你還不知道,那麼下一節會提到這種寫法。雖說如此,但修改網頁程式碼這回事,對於大型網站來說是一個超大工程,光要找出每個頁面中組裝SQL語法之處就得花上不少時間,是否有暫時、快速的解決方案存在呢?有的,只要循著以下幾個步驟,便可讓你的網站暫時脫離這波Mass SQL Injection攻擊,注意!我說的是暫時,這不是長久之計,你應該在做完這些事後,爭取時間修改程式碼。

1、開啟ASP.NET的Custom Error Page功能。

2、降低與 資料庫連線時使用帳號的權限。

3、使用Server端過濾法,濾掉【'】、【--】的輸入。

步驟1及2於MSDN中都能找到說明,此處就不再贅述,步驟3是利用ASP.NET中Global.asax可於要求送達時,第一時間得到送達的表單及URL資訊的特性,於此進行過濾掉可能進行SQL Injection的特殊字元。

<%@ Application Language="C#" %>

<script runat="server">

    protected void Application_BeginRequest(object sender, EventArgs e)
    {
        //scan form values
        foreach (string item in Request.Form.Keys)
        {
            if (Request.Form[item].Contains("'") ||
                Request.Form[item].Contains("--"))
            {
                Response.Redirect("InjectionDetecting.htm");
                return;
            }
        }
        foreach (string item in Request.QueryString.Keys)
        {
            if (Request.QueryString[item].Contains("'") ||
               Request.QueryString[item].Contains("--"))
            {
                Response.Redirect("InjectionDetecting.htm");
                return;               
            }
        }
    }
</script>

只要將此檔案放在Web的根目錄下,那麼透過輸入框或是URL參數以【'】、【--】等手法的SQL Injection動作都會失效,頁面會被導向InjectionDetecting.htm。

 

一勞永逸之參數法

    其實,參數法才是一勞永逸讓網站免於遭受SQL Injection攻擊的唯一良藥,其它的方案只是為你爭取時間,將整個網站中處理SQL指令的部份一一改為參數化,以最初的例子來說,改成參數化後變成下面這樣:

SqlCommand cmd = new SqlCommand(
  "SELECT COUNT(*) FROM USERS WHERE USER_ID = @USER_ID AND PASSWORD = @PASSWORD", conn);
 cmd.Parameters.AddWithValue("@USER_ID", userName);
 cmd.Parameters.AddWithValue("@PASSWORD", password);
 conn.Open();
 return ((int)cmd.ExecuteScalar() > 0);

我知道,要將原本的寫法改為參數化,必須增加一些程式碼,有些程式設計師會因為麻煩,而不願貫徹此寫法,只將此寫法用在重要的如登入系統、訂單處理等介面上,而忽略了查詢介面也有同樣的問題,這就是為何在數年後,我們仍深陷於SQL Injection攻擊的主要原因。所以,收起你的輕忽之心,老老實實的對程式做完整的檢查及調整吧。

 

讓新技術助你一臂之力-LINQ To SQL/LINQ To Entities

 

    拜新技術之賜,未來使用Visual Studio 2008及.NET Framework 3.5來撰寫ASP.NET應用程式,將可輕鬆的藉助LINQ To SQL、LINQ To Entities兩個ORM技術的幫助,完全逃離SQL Injection的威脅。在架構上,這兩個ORM技術都是以參數化的模式將SQL指令送往資料庫,完全的參數化設計,使得SQL Injection將無任何可趁之機,舉個例來說,同樣的登入機制可以寫成下面這樣:

protected void Button1_Click(object sender, EventArgs e)
{
        NorthwindDataContext context = new NorthwindDataContext();
        if( (from s1 in context.USERS where
            s1.USER_ID == TextBox1.Text && s1.PASSWORD == TextBox2.Text select s1).Count() > 0)
            Label1.Text = "歡迎你";
        else
            Label1.Text = "登入失敗";
}

LINQ To SQL所轉換後,送往資料庫的SQL指令如下:

SELECT COUNT(*) AS [value]
FROM [dbo].[USERS] AS [t0]
WHERE ([t0].[USER_ID] = @p0) AND ([t0].[PASSWORD] = @p1)',N'@p0 varchar(5),@p1 varchar(11)',@p0='admin',@p1=''' OR 1=1 --'

如你所見,即使對登入機制進行SQL Injection,也因為參數化SQL 指令而碰璧。

 

方便嗎?LINQ 與組裝式SQL

 

   當將程式改用參數化SQL指令時,免不了的一定會多一些程式碼,這點在LINQ To SQL、LINQ To Entities中不成問題,因為可疊加查詢的設計概念,使得我們在用LINQ To SQL時,可以用類似組裝式SQL概念的方式來撰寫複合查詢,下面是一個例子:

NorthwindDataContext context = new NorthwindDataContext();
var baseData = from s1 in context.Customers select s1;
 if(TextBox1.Text.Length > 0)
       baseData = from s1 in baseData where
                s1.CompanyName.Contains(TextBox1.Text) select s1;
 if (TextBox2.Text.Length > 0)
       baseData = from s1 in baseData where
                s1.CustomerID.Contains(TextBox2.Text) select s1;
 if (TextBox3.Text.Length > 0)
       baseData = from s1 in baseData where
              s1.ContactTitle.Contains(TextBox3.Text) select s1;

LINQ To SQL與LINQ To Entities都有一個共通的特性,在由物件集中取得元素前,也就是進行列舉動作前,允許設計師往同一個物件集中疊加查詢指令。以上例而言,當使用者未於TextBox2中輸入值,而僅輸入TextBox1及TextBox2時,在列舉資料時,LINQ To SQL會送出一段以CompanyName、ContactTitle為查詢條件的SQL指令。請特別注意,即使我們於此處連續對物件集下達三次查詢條件,因為未開始列舉之故,所以只會於列舉時才會送出SQL指令。

 

IN語句

 

   設計師常常在需要IN語句時,以組裝式SQL來處理,原因是要將IN語句參數化有點困難,而且蠻麻煩的,在LINQ To SQL中,IN語句的寫法不但簡單,而且是完全參數化進行的,如下所示:

var result = from s1 in context.Customers where (
         new string[] { "UK", "Lisboa" }).Contains(s1.City) select

 

 

後記

 

    SQL Injection是一個老議題了,希望在這波攻擊結束後,未來我們不會再因大量網站被攻擊後,再次討論SQL Injection。