独上高楼网站
  •    你所在位置:首页 数据库数据库安全经验〉LINQ - 對付 SQL Injection 的 "免費補洞策略"
  • LINQ - 對付 SQL Injection 的 "免費補洞策略"
  • 作者:黃忠成  文章来源:博客园  发布日期:2008-07-11  浏览次数:108
  • 打印这篇文章
  • 作者:黃忠成

    一連串的 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>
    < /tr>
    < tr>
    < td>密碼< /td>
    < td>< asp:TextBox ID="TextBox2" ci="true" MaxLength="12"
    runat="server">< /asp:TextBox>
    < /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(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
    < /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") %>'>
    < /EditItemTemplate>
    < ItemTemplate>
    < asp:Label ID="Label1" runat="server"
    Text='<%# Eval("NOTES") %>'>
    < /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
    < /head>
    < body>
    < form id="form1" runat="server">
    < div>
    < div><%=GetDynamicHtml() %>< /div>
    ..........略..........
    < /div>
    < /form>
    < /body>
    < /html>
    

    .aspx.cs

    protected string GetDynamicHtml()
    {
    string str = "TEST Injection";
    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
    < /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>
    
    < 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") %>'>
    < /EditItemTemplate>
    
    < 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
    < /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") %>'>
    < /EditItemTemplate>
    < ItemTemplate>
    < asp:Label ID="Label1" runat="server"
    Text='<%# GetSafeHtml(Eval("NOTES")) %>'>
    < /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("",
    StringComparison.InvariantCultureIgnoreCase);
    o2 = o2.Replace(o2.Substring(index, 9), " !-->");
    }
    }
    return o2;
    }
    return string.Empty;
    }}
    

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

    圖 18:

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

    <%=GetSafeHtml(GetDynamicHtml()) %>

    關於 validateRequest

    或許你會好奇,ASP.NET 1.1 其之後的版本不是有一個 validateRequest 設定,只要其值為 True,那麼使用者所輸入的值,將會受到 ASP.NET 的限制 (TextBox、及 URL 後帶的參數都在此限),而此值預設為 True,意味著如果你沒有特別去修改 mechine.config 或是 web.config 將此值設為 False,使用者是無法於 TextBox 控件或是 URL 參數中輸入【