LINQ – 對付 SQL Injection 的 “免費補洞策略”

作者:黃忠成

一連串的 Mass SQL Injection 攻擊,讓我們回憶起數年前的 SQL Injection 攻擊,多年後的今天,我們仍深陷於同樣的危機中,本文詳述 SQL Injection 的歷史、肇因、解決及偵測方法,更為讀者們引介全新、更加安全的防堵 SQL Injection 策略。

什麼是 SQL Injection?

SQL Injection,中譯為 SQL 注入,更為人知的名稱是【資料隱碼攻擊】,意指開發人員於撰寫網頁應用程式之際,貪圖一時方便或是依循前人的慣性寫法而開啟的一道門。在數年前,一次大型 的隱碼攻擊行動,喚起了所有網站擁有者及設計人員的防駭之心,讓我們認知到,網站是一個曝露在所有人面前的公共園地,其安全性不容忽視!在那次的攻擊行動 中,有數千個網站遭到同一種手法入侵,洩露的資料及因入侵所損失的金額難以估計,而起源竟只是程式設計師的慣性及疏於防範,而我們都曾經是其中一份子。

那具體上,什麼是 SQL Injection 呢?其實說穿了很簡單,就是透過網頁上的輸入區域 (INPUT 如文字輸入框,或是 URL 中的查詢字串),將特定的 SQL 語句透過網頁送往資料庫執行。以一個登入網頁為例,在設計登入網頁時,我們會放兩個 TextBox 控件,分別讓使用者填入使用者 ID 及密碼,類似畫面如下:

圖 1:

在使用者按下登入按鈕後,我們將其輸入的資訊送往資料庫,驗證使用者輸入的登入資訊是否正確:

using System;
using System.Configuration;
using System.Data;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Data.SqlClient;

public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {

    }

    protected void Button1_Click(object sender, EventArgs e)
    {
        if (ValidateUser(TextBox1.Text, TextBox2.Text))
            Label1.Text = "歡迎你";
        else
            Label1.Text = "登入失敗";
    }

    private bool ValidateUser(string userName, string password)
    {
        SqlConnection conn = new SqlConnection(
             "Data Source=JEFFRAY;Initial Catalog=Northwind;Integrated Security=True");
        using (conn)
        {
            SqlCommand cmd = new SqlCommand("SELECT COUNT(*) FROM USERS WHERE USER_ID = '"
                + userName + "' AND PASSWORD = '" + password + "'", conn);
            conn.Open();
            return ((int)cmd.ExecuteScalar() > 0);
        }
    }
}

當你寫下這些程式碼時,已經開啟了 SQL Injection 的大門了,只要使用者於登入時,填入下圖的資訊,那麼不管 ID 密碼是什麼,一律可以登入系統。

圖 2:

這是為什麼呢?很簡單,起因於下面這行程式碼:

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

我們使用傳統 ASP 常見的手法,以組裝 SQL 指令的方式,將使用者的輸入融入既定的 SQL 語句中,但卻忽略了一件重要的事:使用者可以輸入任意的字串,包括了部份的 SQL 指令!透過輸入部份的 SQL 指令及微調,使用者可以輕易的改變這段 SQL 指令,甚至是疊加另一串 SQL 指令,而我們的網頁則照單全收,以上的輸入,會將整句 SQL 語句調整成下面這樣:

圖 3:

透過必然成真的條件式,再加上 SQL 的註解,我們的網站就這樣曝露在網路上,今天我加的是 OR,若是狠一點的加上 DROP TABLE 等破壞性指令,網站就此拜拜。

這種攻擊不僅僅出現在上例這種 POST 狀況,另一種 GET 狀態也常常受到同樣的攻擊,例如下面的程式碼即開啟了 SQL Injection 的大門。

using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Data.SqlClient;

public partial class QueryStringInjection : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            SqlConnection conn = new SqlConnection("Data Source=JEFFRAY;Initial Catalog=Northwind;Integrated Security=True");
            using (conn)
            {
                SqlCommand cmd = new SqlCommand(
        "SELECT * FROM Customers WHERE CustomerID = '"+Request.QueryString["ID"]+"'", conn);
                conn.Open();
                DetailsView1.DataSource =
                cmd.ExecuteReader(CommandBehavior.CloseConnection);
                DetailsView1.DataBind();
            }
        }
    }
}

試著在 URL 上鍵入:

http://localhost:43236/FirstInjection/QueryStringInjection.aspx?ID=VINET’ OR 1=1 —
註:http://localhost:43236 是你的 Web Development Server 自動產生的 Port,你必須視情況修改。

結果你會看到 CustomerID=”VINET” 以外的 ALFKI 資料列,如下圖:

圖 4:

如果有心人士在 URL 上鍵入 DROP TABLE 或是 INSERT 的 QueryString,將資料任意的刪除或插入惡意的連結 Script (詳見後述的 Mass SQL Injection 一節),那後果不堪設想。

未啟用 Custom Error Page 的漏洞

你應該已經知道,寫 ASP.NET 應用程式的第一道安全手續就是啟用 Custom Error Page 功能,讓駭客們無法透過預設的錯誤網頁來取得不該取得的資訊,若未啟用 Custom Error Page,那麼下圖是可能發生在你的網站中的:

圖 5:

有了這些資訊,具有耐心的駭客,要透過輸入不同的字元來探測整段 SQL 語句就不困難了,防堵的最佳辦法就是啟用 Custom Error Page 設定:

Web.config
...............略
<customErrors mode="On" defaultRedirect="DefaultError.htm">
</customErrors>
............略

一旦啟用後,錯誤發生時會導向 DefaultError.html,結果變成下面這樣:

圖 6:

檢測你的網頁有無 SQL Injection 的可能性

OK,那有沒有辦法可以檢測現在的網頁是否受 SQL Injection 威脅呢?如果你是網站管理者,而非設計師,那麼你只有依賴現在常見的網頁漏洞檢測工具,對網頁進行黑箱測試,不過提醒你,目前的網頁漏洞測試工具大多是針對 PHP、ASP 所設計的,能測出來的漏洞相當有限,有時即使是安全的網頁,也會因為未實作過濾法(後述),而導致誤判。

如果你是程式設計師,事情就簡單的多了,只要檢視一下程式碼,看看動態組裝 SQL 語句的部份是否有 SQL Injection 即可,圖 007 是一個確認 SQL Injection 是否存在於你的程式中的公式。

圖 7:

只要你的程式中,有 SQL 字串加上使用者輸入值的情況,那麼該網頁存在 SQL Injection 危機的可能性就高達 99.9%。

前輩的叮嚀:防止 SQL Injection 的方法

在數千個網站的入侵事件發生後,許多資安專家提出了各種防範 SQL Injection 的方法,其中不外乎圖 008 的四種。

圖 8:

過濾法可以阻止特定字如【–】、【 OR 】、【’】的輸入,能有效防堵必然成真條件式及錯誤訊息顯示時的漏洞,不過魔高一丈,此法最後仍然遭受破解,透過 SQL 的轉碼函式,駭客可以將部份 SQL 語句做出編碼來逃避偵測,最後突破這道防線。但由於轉碼後的字串相當長,所以只要設計師細心些,搭配 MaxLength 的設定,還是可以讓過濾法奏效,但過濾法其實很脆弱,所以一定要搭配其它的手法方能行之。

下面是一個使用過濾法的例子,利用引入外部 JavaScript 檔案及 Form 的 onSubmit 事件,在送出資料前先檢測擁有 ci Attribute 標示的 text tag,此法可運行於 IE 及 FireFox 上:

Injectiondetect.js

function validateInjection()
{
          var i = 0;
          for(i = 0; i < document.forms[0].elements.length;i++)
          {
            if(document.forms[0].elements[i].type == 'text' &&
               document.forms[0].elements[i].getAttribute("ci") != null)
            {
              var elem = document.forms[0].elements[i];
              if(elem.value != null &&
                 (elem.value.indexOf('\'') != -1 ||
                  elem.value.indexOf('--') != -1 ||
                  elem.value.indexOf(' OR ') != -1))
              {
                alert('possible injection detected.')
                return false;
              }
            }
          }
          return true;
}

.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="DefaultWithFilter.aspx.cs" Inherits="DefaultWithFilter" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Untitled Page</title>
    <script language='javascript' type="text/javascript" src='injectiondetect.js'>
    </script>
</head>
<body>
    <form id="form1" onsubmit="return validateInjection()" runat="server">
    <div>
        <table border="1">
        <tr>
        <td>使用者編號</td>
        <td><asp:TextBox ID="TextBox1" ci="true" MaxLength="12"
                     runat="server"></asp:TextBox></td>
        </tr>
        <tr>
        <td>密碼</td>
        <td><asp:TextBox ID="TextBox2" ci="true" MaxLength="12"
                 runat="server"></asp:TextBox></td>
        </tr>
        <tr>
        <td colspan=2>
            <asp:Button ID="Button1" runat="server" Text="登入" onclick="Button1_Click" />
        </td>
        </tr>
        </table>
        <asp:Label ID="Label1" runat="server" Text=""></asp:Label>
    </div>
    </form>
</body>
</html>

下圖是嘗試於此網頁進行 SQL Injection 攻擊時的結果:

圖 9:

不過,這種過濾法還不完善,因為資深的駭客仍然可以透過將網頁存成 HTML,移除 JavaScript 認證並假造 ViewState 來對網站進行 SQL Injection 攻擊!所以,完善的過濾法應該是 Client 端與 Server 都有,Server 端如下所示:

.aspx.cs

using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Data.SqlClient;

public partial class DefaultWithFilter : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {

    }
    private bool DetectInjection(string input)
    {
        if (input.IndexOf("'") != -1 ||
           input.IndexOf("--") != -1 ||
           input.IndexOf(" OR ") != -1)
            return true;
        return false;
    }
    protected void Button1_Click(object sender, EventArgs e)
    {
        if (TextBox1.Text.Length > 12 ||
            TextBox2.Text.Length > 12 ||
            DetectInjection(TextBox1.Text) ||
            DetectInjection(TextBox2.Text))
        {
            ClientScript.RegisterStartupScript(typeof(Page), "Alert_Msg",
                 "alert('possible injection detected.')", true);
            return;
        }
        if (ValidateUser(TextBox1.Text, TextBox2.Text))
            Label1.Text = "歡迎你";
        else
            Label1.Text = "登入失敗";
    }

    private bool ValidateUser(string userName, string password)
    {
        SqlConnection conn = new SqlConnection(
        "Data Source=JEFFRAY;Initial Catalog=Northwind;Integrated Security=True");
        using (conn)
        {
            SqlCommand cmd = new SqlCommand("SELECT COUNT(*) FROM USERS WHERE USERS.USER_ID = '"
                + userName + "' AND USERS.PASSWORD = '" + password + "'", conn);
            conn.Open();
            return ((int)cmd.ExecuteScalar() > 0);
        }
    }
}

或許你會覺得實作起來挺麻煩的,但這是過濾法所需付出的代價!

除了過濾法外,使用低權限的帳號連結資料庫也是安全常識之一,藉由降低連線帳號的權限,可以讓 DROP TABLE 等破壞力超強的手法碰壁,不過這種手法不應該成為唯一防堵 SQL Injection 的方式,因為你不可能連 INSERT 都不給執行,而 INSERT 是駭客入侵網頁的常見手法。

使用 Parameter 是目前已知,一勞永逸逃離 SQL Injection 的手法,將前述的程式調整成下面這樣,即可讓其完全逃離 SQL Injection。

using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Data.SqlClient;

public partial class Default2 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {

    }
    protected void Button1_Click(object sender, EventArgs e)
    {
        if (ValidateUser(TextBox1.Text, TextBox2.Text))
            Label1.Text = "歡迎你";
        else
            Label1.Text = "登入失敗";
    }

    private bool ValidateUser(string userName, string password)
    {
        SqlConnection conn = new SqlConnection(
                 "Data Source=JEFFRAY;Initial Catalog=Northwind;Integrated Security=True");
        using (conn)
        {
            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 的方法,在業界已經流傳許久,其中使用 Parameter 更是快變成常識級的考古用知識。那為何到目前為止,你仍然時常聽到某某網站遭受 SQL Injection 攻擊,甚至!你只要有足夠的時間及耐心,用 Google 以關鍵字【登入】、【資料查詢】查詢,接著以上述的【’ OR 1=1 –】或是【’】來一一測試,輕輕鬆鬆就能找到幾個吃這套技巧的網站,然後取得極度敏感的個人資料或是其用來查詢的 SQL 字串。

SQL Injection 至今仍然存在的原因很簡單,程式設計師的惰性、慣性及大而化之的個性,是導致 SQL Injection 存在於這個高安全性當道時代的最大原因。

雖然使用 Parameter 手法可以防掉所有的 SQL Injection 攻擊,但在此同時,也增加了需要撰寫的程式碼長度,常見的結果便是設計師只會在特定敏感功能上,才會使用這種手法。

在現時今日,你很難當起駭客,透過 SQL Injection 手法通過極大多數網站的【登入】機制,原因不是程式設計師及網站管理者的細心,而是這個機制曾經出了個所有人都無法忽視的大包, 但 SQL Injection 只出現在這些機制上嗎?你我都知道,這不是真的,舉個實例來說,以下的查詢網頁是相當常見的。

圖 10:

這個網頁允許使用者任意輸入【公司名稱】、【客戶編號】、【聯絡人】等三個欄位之搜尋字串,然後進行組合查詢,如果使用者僅輸入【公司名稱】,那麼系統將不會把其它兩個欄位放入查詢語句中,請先閉上眼睛,思考著你怎麼實現這個功能,如果結果是下面這樣,那你已然開啟 SQL Injection 的大門了。

protected void Button1_Click(object sender, EventArgs e)
{
        SqlConnection conn = new SqlConnection(
          "Data Source=JEFFRAY;Initial Catalog=Northwind;Integrated Security=True");
        using (conn)
        {
            SqlCommand cmd = new SqlCommand("SELECT * FROM Customers", conn);
            string cmdStr = "";
            if(TextBox1.Text.Length > 0)
                cmdStr += string.Format(" CompanyName LIKE '%{0}%' AND ",TextBox1.Text);
            if(TextBox2.Text.Length > 0)
                cmdStr += string.Format(" CustomerID LIKE '%{0}%' AND ",TextBox2.Text);
            if(TextBox3.Text.Length > 0)
                cmdStr += string.Format(" ContactTitle LIKE '%{0}%' AND ",TextBox3.Text);
            if (cmdStr.Length > 0)
                cmd.CommandText += " WHERE " + cmdStr.Substring(0, cmdStr.Length - 5);

            conn.Open();
            GridView1.DataSource = cmd.ExecuteReader(CommandBehavior.CloseConnection);
            GridView1.DataBind();
        }
}

導致我們寫下這個程式的原因有三個:

圖 11:

第三個原因大概是寫下此程式之設計師真正的想法,那會變多長呢?我們試著寫一下就知道了。

protected void Button1_Click(object sender, EventArgs e)
    {
        SqlConnection conn = new SqlConnection(
          "Data Source=JEFFRAY;Initial Catalog=Northwind;Integrated Security=True");
        using (conn)
        {
            SqlCommand cmd = new SqlCommand("SELECT * FROM Customers", conn);
            string cmdStr = "";
            if (TextBox1.Text.Length > 0)
            {
                cmdStr += " CompanyName LIKE @CName AND ";
                cmd.Parameters.AddWithValue("@CName", "%"+TextBox1.Text+"%");
            }
            if (TextBox2.Text.Length > 0)
            {
                cmdStr += " CustomerID LIKE @CID AND ";
                cmd.Parameters.AddWithValue("@CID", "%" + TextBox2.Text + "%");
            }
            if (TextBox3.Text.Length > 0)
            {
                cmdStr += " ContactTitle LIKE @CTitle AND ";
                cmd.Parameters.AddWithValue("@CTitle", "%" + TextBox3.Text + "%");
            }
            if (cmdStr.Length > 0)
                cmd.CommandText += " WHERE " + cmdStr.Substring(0, cmdStr.Length - 5);
            conn.Open();
            GridView1.DataSource = cmd.ExecuteReader(CommandBehavior.CloseConnection);
            GridView1.DataBind();
        }
    }

唔,也不是很長嘛,為了省幾行程式碼開這麼大的洞,值得嗎?呵,程式設計師就是這種動物,不是嗎?

補洞的代價

在你暗笑著,我怎麼可能會犯下前節的錯誤時,我必須提醒你程式設計師的第二個通病,那就是【以己度人】,白話就是【我不會犯的錯,所以別人也不會犯】,在 群組開發時,這個情況更是履見不鮮。事實是,你不會寫下這段程式碼,但難保其它設計師不會,畢竟都是糊口飯吃的,只要補洞的工作需要付出時間代價,那麼就 一定有人會偷懶或粗心,肇因可能是惰性、慣性,也可能是你未跟他明確提及這事兒的嚴重性,而後果常是我們無法承受的。

LINQ To SQL/LINQ To Entities = “免費的補洞策略”

除了程式設計師努力的防堵 SQL Injection 之外,開發平台廠商也沒有置身事外,以 Microsoft .NET 平台來說,為了防堵 SQL Injection,ASP.NET Team 於 ASP.NET 2.0 推出了 DataSource 控件群,這組控件利用了 Parameter 的手法,完全避開了 SQL Injection 的發生。但是我依舊時常聽到許多設計師抱怨:【DataSource 控件用起來綁手綁腳的,內部不知道在搞什麼,還不如自己用 SqlCommand 來得快且直覺。】,的確,事實是如此,DataSource 控件為了防堵 SQL Injection 所做的努力,換來的是【綁手綁腳】的惡名,這該歸咎於 ASP.NET Team 設計 DataSource 控件時,沒有多花點心思在易用性、安全性、彈性上取得平衡點。

終於,在 .NET Framework 3.5 及 Visual Studio 2008 上市後,兩個免費、快速、有效的補洞策略出現在我們眼前,他們就是【LINQ To SQL/LINQ To Entities】

說實話!這兩個技術都不是為了 SQL Injection 而誕生的,但其預設以 Parameter 手法運行的設計,卻給了我們一個新的防堵 SQL Injection 的方法,更好的是,程式設計師不但不用為了防堵 SQL Injection 寫更多的程式碼,相反的程式碼還變少了,以最初的登入機制為例,用 LINQ To SQL 改寫後變成下面這樣:

using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;

public partial class LoginWithLINQ : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {

    }
    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 會將 LINQ 運算式轉成 SQL 語句,然後將條件式一一以參數描述之故,透過 SQL Profiler 工具可得知,上面的程式最終送往 SQL Server 執行的語句如下:

exec sp_executesql N'SELECT COUNT(*) AS [value]
FROM [dbo].[USERS] AS [t0]
WHERE ([t0].[USER_ID] = @p0) AND ([t0].[PASSWORD] = @p1)',N'@p0 varchar(5),@p1 varchar(4)',@p0='admin',@p1='test'

很明顯的,這個技巧完全沒有 SQL Injection 的危機存在。

想加來加去,又不要 SQL Injection 嗎?LINQ 做給你

複合查詢常讓設計師不得不採用組裝式 SQL 語句手法,而其結果也常因為使用 Parameter 會導致程式碼變複雜,而循傳統手法完成該功能,最後留下 SQL Injection 的漏洞。那使用 LINQ To SQL/LINQ To Entities 來改寫的話,真的可以避免 SQL Injection 及簡化程式碼嗎?讓實例說話吧,我們以上例的複合查詢為例,改寫成 LINQ To SQL 版本之程式碼如下 :

protected void Button1_Click(object sender, EventArgs e)
{
        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;
        GridView1.DataSource = baseData;
        GridView1.DataBind();
}

此例中,我利用了 LINQ To SQL/LINQ To Entities 只在列舉資料集元素前,才會開始組裝 SQL 語句的共通行為,以疊加式查詢的方式來完成複合查詢的工作,請特別注意,這段程式碼只會送出一段 SQL 語句,不是四個,透過 SQL Profiler 可以證明這點:

exec sp_executesql N'SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address],
[t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax], [t0].[NOTES], [t0].[TEST_ID]
FROM [dbo].[Customers] AS [t0]
WHERE ([t0].[CustomerID] LIKE @p0) AND ([t0].[CompanyName] LIKE @p1)',N'@p0 nvarchar(4),@p1 nvarchar(3)',@p0=N'%FR%',@p1=N'%V%'

讓事實說話:LINQ To SQL VS SQL Injection

我說你不一定信,你可以下載範例,然後對本文所提及的兩個 LINQ To SQL 網頁進行 SQL Injection 的測試,圖 012 是以【’ OR 1=1 –】手法來測試登入機制:

圖 12:

圖 013 是複合查詢的測試:

圖 13:

改用 LINQ To Entities 也是一樣的結果。

幫幫忙,別自己開洞,ExecuteQuery 及 ExecuteCommand

那使用 LINQ To SQL/LINQ To Entities 就能保證不被 SQL Injection 所擾了嗎?那可不一定,因為設計師還是常常會貪一時方便,開啟 SQL Injection 的大門。基於彈性,LINQ To SQL 及 LINQ To Entities 都支援直接將 SQL 語句送往資料庫執行的機制,LINQ To SQL 的 ExecuteQuery 就是一個例子:

protected void Button1_Click(object sender, EventArgs e)
{
        NorthwindDataContext context = new NorthwindDataContext();
        string str = "SELECT * FROM USERS WHERE USER_ID = '" + TextBox1.Text +
            "' AND PASSWORD = '" + TextBox2.Text + "'";
        int ret = context.ExecuteQuery<USERS>(str).Count();
        if (ret > 0)
            Label1.Text = "歡迎你";
        else
            Label1.Text = "登入失敗";
}

所以,要防堵 SQL Injection,使用 LINQ To SQL/LINQ To Entities 是最具成效及具經濟效益的,不過前提是設計師得幫幫忙,別放著有新的方便且有效率的技巧不學,故意去當打洞工人。

迷思:Stored Procedure 是安全的,Parameter 是無敵的?

的確,是我告訴你,使用 Parameter 是防堵 SQL Injection 最快、最有效、最完整的手法,但是前題是不能與 Stored Procedure 扯上關係!基於網路上的片段資料,設計師總覺得,如果我使用了 Stored Procedure,並使用 Parameter 來傳遞參數,那就對 SQL Injection 完全免疫了!所有軟體專案主導者都同意,當要求不明確時,結果也會不明確,基於防堵 SQL Injection 的大前題下,許多專案主導者都會要求設計師不要在程式中組裝 SQL 語句,而改用 Stored Procedure,但!他們卻高估了程式設計師的的理解力,天才工程師以 Stored Procedure 來處理複合查詢,寫下程式碼如下:

protected void Button1_Click(object sender, EventArgs e)
{
        SqlConnection conn = new SqlConnection(
       "Data Source=JEFFRAY;Initial Catalog=Northwind;Integrated Security=True");
        using (conn)
        {
            SqlCommand cmd = new SqlCommand("QueryCustomers", conn);
            cmd.CommandType = CommandType.StoredProcedure;
            cmd.Parameters.AddWithValue("@CompanyName",
                     TextBox1.Text.Length == 0 ? "" : TextBox1.Text);
            cmd.Parameters.AddWithValue("@CustomerID",
                     TextBox2.Text.Length == 0 ? "" : TextBox2.Text);
            cmd.Parameters.AddWithValue("@ContactTitle",
               TextBox3.Text.Length == 0 ? "" : TextBox3.Text);
            conn.Open();
            GridView1.DataSource = cmd.ExecuteReader(CommandBehavior.CloseConnection);
            GridView1.DataBind();
        }
}

正如你所見,這是一段看不出問題在那的程式碼,直到看到了 QueryCustomers 的預存程序,你會吐血:

CREATE PROCEDURE dbo.QueryCustomers
	(
	@CompanyName nvarchar(30),
	@CustomerID nvarchar(12),
	@ContactTitle nvarchar(30)
	)
AS
    DECLARE @STR nvarchar(255)
    DECLARE @WK nvarchar(255)
    SET @STR = 'SELECT * FROM Customers'
    SET @WK = ''

    IF NOT @CompanyName IS NULL
       SET @WK = @WK + ' CompanyName LIKE ''%'+@CompanyName+'%'' AND  '

    IF NOT @CustomerID IS NULL
       SET @WK = @WK + ' CustomerID LIKE ''%'+@CustomerID+'%'' AND  '

    IF NOT @ContactTitle IS NULL
       SET @WK = @WK + ' ContactTitle LIKE ''%'+@ContactTitle+'%'' AND  '

    IF LEN(@STR) > 0
    BEGIN
       SET @STR = @STR+' WHERE '+SUBSTRING(@WK,0,LEN(@WK)-3)
       exec sp_executesql @STR
    End
    ELSE
       exec sp_executesql @STR

結果就是:

圖 14:

一旦變成這樣,就算是 LINQ To SQL/LINQ To Entities 也無法救你脫離 SQL Injection 的威脅!所以,你應該明確的告訴設計師,SQL Injection 是由組裝式 SQL 語句而引發的,得注意任何有【組裝式 SQL】發生的程式碼,這當然也包含了 Stored Procedure。此例正確的 Stored Procedure 寫法如下:

CREATE PROCEDURE dbo.SafeQueryCustomers
	(
	@CompanyName nvarchar(30),
	@CustomerID nvarchar(12),
	@ContactTitle nvarchar(30)
	)
AS
    DECLARE @STR nvarchar(255)
    DECLARE @WK nvarchar(255)
    SET @STR = 'SELECT * FROM Customers'
    SET @WK = ''

    IF NOT @CompanyName IS NULL
    BEGIN
       SET @WK = @WK + ' CompanyName LIKE @pCompanyName AND  '
       SET @CompanyName = '%' +@CompanyName + '%'
    END   

    IF NOT @CustomerID IS NULL
    BEGIN
       SET @WK = @WK + ' CustomerID LIKE @pCustomerID AND  '
       SET @CustomerID = '%' +@CustomerID + '%'
    END   

    IF NOT @ContactTitle IS NULL
    BEGIN
       SET @WK = @WK + ' ContactTitle LIKE @pContactTitle AND  '
       SET @ContactTitle = '%' +@ContactTitle + '%'
    END    

    IF LEN(@STR) > 0
    BEGIN
       SET @STR = @STR+' WHERE '+SUBSTRING(@WK,0,LEN(@WK)-3)
       exec sp_executesql @STR,
N'@pCompanyName nvarchar(30),@pCustomerID nvarchar(12),@pContactTitle nvarchar(30)',
@pCompanyName=@CompanyName,@pCustomerID=@CustomerID,@pContactTitle=@ContactTitle
    End
    ELSE
       exec sp_executesql @STR

使用參數是防堵 SQL Injection 的不二法門,就算是在 Stored Procedure 中亦是如此。

魔高一丈:新一代的 Mass SQL Injection 手法

網頁的攻擊威脅中,SQL Injection 算是相當狠毒的手法,因為破壞性高,所以也就更引人注目,但 SQL Injection 是一種手法的統稱,近日各大報的報導,相信大多人都已見識到新一代的 SQL Injection 手法,統稱為【Mass SQL Injection】

此手法是利用了傳統的 SQL Injection 為進入點,再利用設計師對於控件行為的熟悉度不足,及忽視網頁安全性來侵入。舉個例來說,我們常用 GridView 等控件來顯示資料,為了某些理由,我們會直接以<%# %>方式來輸出列資料,如下所示:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="InputInjection.aspx.cs" Inherits="InputInjection" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>

        <asp:LinqDataSource ID="LinqDataSource1" runat="server"
            ContextTypeName="NorthwindDataContext"
            Select="new (CustomerID, CompanyName, NOTES)" TableName="Customers">
        </asp:LinqDataSource>
        <asp:GridView ID="GridView1" runat="server" AllowPaging="True"
            AutoGenerateColumns="False" DataSourceID="LinqDataSource1">
            <Columns>
                <asp:BoundField DataField="CustomerID"
                      HeaderText="CustomerID" ReadOnly="True"
                    SortExpression="CustomerID" />
                <asp:BoundField DataField="CompanyName" HeaderText="CompanyName"
                    ReadOnly="True" SortExpression="CompanyName" />
                <asp:TemplateField HeaderText="NOTES" SortExpression="NOTES">
                    <EditItemTemplate>
                        <asp:Label ID="Label1" runat="server"
                         Text='<%# Eval("NOTES") %>'></asp:Label>
                    </EditItemTemplate>
                    <ItemTemplate>
                        <asp:Label ID="Label1" runat="server"
                          Text='<%# Eval("NOTES") %>'></asp:Label>
                    </ItemTemplate>
                </asp:TemplateField>
            </Columns>
        </asp:GridView>

    </div>
    </form>
</body>
</html>

很平常的程式碼不是,結果也很平常:

圖 15:

直到你在第一筆資料的 NOTES 欄位中鍵入下面的文字:

<script>alert('test')</script>

結果將會有所改變:

圖 16:

歡迎來到 Mass SQL Injection 的世界,Mass SQL Injection 利用了設計師常忽略了輸出資料時要使用 Html Encode 機制的常識,而進行了非毀滅性的入侵,這種入侵常見於討論區、部落格或留言版上,他的目的並非毀滅此網站,而只是利用此漏洞來當掉此網站,亦或是將此網站做為跳板,以 Cross-Site Scripting (簡稱為 XSS) 的方式,將使用者所輸入的資料,導向另一網站,而這個網站通常是駭客們所建構的,無辜瀏覽此網站的人所輸入的資料就這樣被偷走了。

另一種更狠毒的手法則是利用 Cross-Site Scripting 的方式,在網頁中注入惡意連結的 Script,透過作業系統的漏洞如 Windows 的 MS06-014、MS07-004 來入侵使用者的電腦,將網站當成跳板是第一步,接著再把瀏覽器當成第二跳版,最後入侵使用者的電腦。

當然,你可能會說,我常用的是 Bind,不是 Eval 這個函式,那麼我告訴你,這兩者有同樣的問題存在。就算不用到 Eval 或是 Bind,你偶而也會寫成下面這樣子:

.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="InputInjection.aspx.cs" Inherits="InputInjection" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <div><%=GetDynamicHtml() %></div>
        ..........略..........
    </div>
    </form>
</body>
</html>

.aspx.cs

protected string GetDynamicHtml()
{
        string str = "<b>TEST Injection</b>";
        return str;
}

想像一下,如果 GetDynamicHtml 函式中的 str 變數值是由資料庫取得後填入的,你是否有背脊發涼的感覺了。

如何防止?

防堵 Mass SQL Injection 的方法其實很簡單,除了以 Parameter 或是 LINQ To SQL/LINQ To Entities 來阻檔傳統的 SQL Injection 攻擊外,只要在輸出資料庫資料時,記得用 HtmlEncode 即可:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="InputInjection.aspx.cs" Inherits="InputInjection" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>

        <asp:LinqDataSource ID="LinqDataSource1" runat="server"
            ContextTypeName="NorthwindDataContext"
            Select="new (CustomerID, CompanyName, NOTES)" TableName="Customers">
        </asp:LinqDataSource>
        <asp:GridView ID="GridView1" runat="server" AllowPaging="True"
            AutoGenerateColumns="False" DataSourceID="LinqDataSource1">
            <Columns>
                <asp:BoundField DataField="CustomerID"
                        HeaderText="CustomerID" ReadOnly="True"
                    SortExpression="CustomerID" />
                <asp:BoundField DataField="CompanyName" HeaderText="CompanyName"
                    ReadOnly="True" SortExpression="CompanyName" />
                <asp:TemplateField HeaderText="NOTES" SortExpression="NOTES">
                    <EditItemTemplate>
                        <asp:Label ID="Label1" runat="server"
                            Text='<%# Eval("NOTES") %>'></asp:Label>
                    </EditItemTemplate>
                    <ItemTemplate>
                        <asp:Label ID="Label1" runat="server" Text=
       '<%# Server.HtmlEncode(Eval("NOTES") == null ? "" : Eval("NOTES").ToString()) %>'>
         </asp:Label>
                    </ItemTemplate>
                </asp:TemplateField>
            </Columns>
        </asp:GridView>

    </div>
    </form>
</body>
</html>

圖 17:

那如果執意輸出 HTML 呢?那我只能建議你,在輸出前查查輸出的字串中是否有 script 字樣,並注意 onclick、onkeydown、onblur 等事件及 src tag 的輸出,沒有必要的話,就把所有的事件處理式濾掉,這樣才能讓你逃出 Mass SQL Injection 的攻擊,下面是一個簡單的例子,允許除 script 外的 HTML 輸出。

.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="InputInjection2.aspx.cs" Inherits="InputInjection2" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>

        <asp:LinqDataSource ID="LinqDataSource1" runat="server"
            ContextTypeName="NorthwindDataContext"
            Select="new (CustomerID, CompanyName, NOTES)" TableName="Customers">
        </asp:LinqDataSource>
        <asp:GridView ID="GridView1" runat="server" AllowPaging="True"
            AutoGenerateColumns="False" DataSourceID="LinqDataSource1">
            <Columns>
                <asp:BoundField DataField="CustomerID"
                     HeaderText="CustomerID" ReadOnly="True"
                    SortExpression="CustomerID" />
                <asp:BoundField DataField="CompanyName" HeaderText="CompanyName"
                    ReadOnly="True" SortExpression="CompanyName" />
                <asp:TemplateField HeaderText="NOTES" SortExpression="NOTES">
                    <EditItemTemplate>
                        <asp:Label ID="Label1" runat="server"
                            Text='<%# Eval("NOTES") %>'></asp:Label>
                    </EditItemTemplate>
                    <ItemTemplate>
                        <asp:Label ID="Label1" runat="server"
                           Text='<%# GetSafeHtml(Eval("NOTES")) %>'></asp:Label>
                    </ItemTemplate>
                </asp:TemplateField>
            </Columns>
        </asp:GridView>
    </div>
    </form>
</body>
</html>

.aspx.cs

using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;

public partial class InputInjection2 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
    }

    protected string GetSafeHtml(object o)
    {
        if (o != null)
        {
            string o2 = (string)o;
            if (o2.IndexOf("<script",StringComparison.InvariantCultureIgnoreCase) != -1)
            {
                while (true)
                {
                    int index = o2.IndexOf("<script",
                              StringComparison.InvariantCultureIgnoreCase);
                    if (index == -1) break;
                    o2 = o2.Replace(o2.Substring(index, 8), "<!-- ");
                    index = o2.IndexOf("</script>",
                        StringComparison.InvariantCultureIgnoreCase);
                    o2 = o2.Replace(o2.Substring(index, 9), " !-->");
                }
            }
            return o2;
        }
        return string.Empty;
    }}

當 NOTES 值為【<b>test</b>】時,輸出結果如下:

圖 18:

GetSafeHtml 也適用於前例的 GetDynamicHtml 情況下:

<div><%=GetSafeHtml(GetDynamicHtml()) %></div>

關於 validateRequest

或許你會好奇,ASP.NET 1.1 其之後的版本不是有一個 validateRequest 設定,只要其值為 True,那麼使用者所輸入的值,將會受到 ASP.NET 的限制 (TextBox、及 URL 後帶的參數都在此限),而此值預設為 True,意味著如果你沒有特別去修改 mechine.config 或是 web.config 將此值設為 False,使用者是無法於 TextBox 控件或是 URL 參數中輸入【<script&gtalert(‘test’)</script】等字樣的,那也就沒有 Mass SQL Injection 危機了不是嗎?

Web.config

<system.web>
  ............
  <pages buffer="true" validateRequest="true" />
  ............
</system.web>

圖 19:

的確,但這樣一來,連【<b>TEST</b>】等輸入也一併被防掉了,在許多討論區及留言版中,這種輸入是必要的!況且,ASP.NET 1.x 在 validateRequest 的驗證上有著 BUG,若你未更新 ASP.NET 2.0 或是最新的 ASP.NET 1.x Service Pack,那麼 Mass SQL Injection 仍然與你長伴。

那如果用的已是 ASP.NET 2.0,且更新到最新版並將 validateRequest 設為 Ture 後,是否就可高枕無憂了呢?這我不能保證,因為在BUG未被發現前,都不叫 BUG!而且在這個 AJAX 盛行的年代,以 XMLHttpRequest 穿透 Validate Request 機制並不是不可能。所以,validateRequest 絕不能做為防止 Mass SQL Injection 的唯一手段,應整合 SQL Injection 的所有防範手段,至少必須使用 Parameter 或是 LINQ To SQL/LINQ To Entities 來與資料庫溝通,才是正道。

後記

因為網頁是隨手可得,所以成為了駭客們的玩具,本文所提及的僅是關於 SQL Injection 的手法,其它如 DDoS 等攻擊還是很多,身為網頁設計師的我們,除了必須細心之外,還得時常上一些安全性回報網站來擷取新知,對於駭客,我們必須常保初學者心態,因為駭客們就是抓住程式設計師自持身經百戰的傲氣,而趁虛而入。

範例解說:

檔名 說明
Default.aspx 無設防之 Login 網頁 (有 SQL Injection 危機)。
Default2.aspx 設防之 Login 網頁 (使用參數,無 SQL Injection 危機)。
DefaultWithFilter.aspx 設防之 Login 網頁,使用過濾法。
QueryStringInjection.aspx 無設防之 QueryString 網頁(有 SQL Injection 危機)。
InputInjection.aspx Mass SQL Injection 測試網頁 (已設防,請自行調整為不設防來測試),使用LINQ To SQL。
InputInjection2.aspx Mass SQL Injection 測試網頁 2 (已設防,採用過濾 script 手法) ,使用 LINQ To SQL。
QueryData.aspx 簡單資料查詢頁面 (無設防,有 SQL Injection 危機)。
QueryData2.aspx 簡單資料查詢頁面 (已設防,使用參數,無 SQL Injection 危機)。
QueryData3.aspx 複合查詢頁面 (無設防,包含無設防及有設防之 Stored Procedure 手法)。
QueryData4.aspx 複合查詢頁面 (已設防,無 SQL Injection 危機)。
LoginWithLINQ.aspx 設防之 Login 網頁,使用 LINQ To SQL。
QueryData4WithLinq.aspx 設防之複合查詢頁面,使用 LINQ To SQL。

範例下載

(範例為 Visual Studio 2008 網站專案,資料庫使用 SQL 2005 Express,方便測試)
http://www.dreams.idv.tw/~code6421/files/SInjectionDemo.zip

(Visual Studio 2005 用網站專案,移除 LINQ To SQL 部份網頁)
http://www.dreams.idv.tw/~code6421/files/SInjectionDemo2005.zip

參考資料:

由胡百敬老師所寫的詳細 SQL Injection 說明

其它 MSDN 相關文件

安全性更新

關於LINQ To SQL/LINQ To Entities 的書籍

與作者聯絡

相关日志

发表评论