天天看點

一個較完整的關鍵字過濾解決方案(上)

    前不久寫了篇利用httpmodule來實作防sql的注入,其主要思想就是在頁面通路隻初就對Get和post兩種方式送出的資料進行驗證,今天發現老趙寫的關鍵字過濾解決方案,思想基本一樣,但寫得比較深入,特此轉載

原文位址:http://www.cnblogs.com/JeffreyZhao/archive/2008/12/22/filter-forbidden-word-solution.html

如果您希望看到關鍵字過濾算法的話那麼可能就要失望了。部落格園中已經有不少關于此類算法的文章(例如這裡和這裡),雖然可能無法直接滿足特定需求,但是已經足夠作為參考使用。而本文的目的,是給出一個較為完整的關鍵字過濾功能,也就是将使用者輸入中的敏感字元進行替換——這兩者有什麼差別?那麼就請繼續看下去吧。:)

有趣的需求

  關鍵字過濾功能自然無比重要,但是如果要在代碼中對每個輸入進行檢查和替換則會是一件非常費神費事的事情。尤其是如果網站已經有了一定規模,使用者輸入功能已經遍及各處,而急需對所有輸入進行關鍵字過濾時,上述做法更可謂“遠水解不了近渴”。這時候,如果有一個通用的辦法,呼得一下為整站的輸入加上了一道屏障,那該是一件多麼惬意的事情。這就是本文希望解決的問題。是不是很簡單?我一開始也這麼認為,不過事實上并非那麼一帆風順,而且在某些特定條件下似乎更是沒有太好的解決方法……

  您慢坐,且聽我慢慢道來……

實作似乎很簡單

  資料結構中的單向連結清單可謂無比經典。有人說:單向連結清單的題目好難啊,沒法逆序查找,很多東西都不容易做。有人卻說:單向連結清單既然隻能向一個方向周遊,那麼變化就會很有限,是以題目不會過于複雜。老趙覺得後者的說法不無道理。例如在現在的問題上,我們如果要在一個ASP.NET應用程式中做一個統一的“整站方案”,HttpModule似乎是唯一的選擇。

  思路如下:我們在Request Pipeline中最早的階段(BeginRequest)将請求的QueryString和Form集合中的值做過濾,則接下來的ASP.NET處理過程中一切都為“規範”的文字了。說幹就幹,不就是替換兩個NameValueCollection對象中的值嗎?這再簡單不過了:

public class FilterForbiddenWordModule : IHttpModule
{
    void IHttpModule.Dispose() { }

    void IHttpModule.Init(HttpApplication context)
    {
        context.BeginRequest += new EventHandler(OnBeginRequest);
    }

    private static void OnBeginRequest(object sender, EventArgs e)
    {
        var request = (sender as HttpApplication).Request;
        ProcessCollection(request.QueryString);
        ProcessCollection(request.Form);
    }

    private static void ProcessCollection(NameValueCollection collection)
    {
        var copy = new NameValueCollection();

        foreach (string key in collection.AllKeys)
        {
            Array.ForEach(
                collection.GetValues(key),
                v => copy.Add(key, ForbiddenWord.Filter(v)));
        }

        collection.Clear();
        collection.Add(copy);
    }
}
      

  在BeginRequest階段,我們将調用ProcessCollection将QueryString和Form兩個NameValueCollection中的值使用ForbiddenWord.Filter方法進行處理。ForbiddenWord是一個靜态類,其中的Filter方法會将原始字元串中的敏感字元使用“**”進行替換。替換方法不在本文的讨論範圍内,是以我們就以如下方式進行簡單替換:

public static class ForbiddenWord
{
    public static string Filter(string original)
    {
        return original.Replace("FORBIDDEN_WORD", "**");
    }
}
      

  看似沒有問題,OK,随便打開一張頁面看看……

Collection is read-only.

Description: An unhandled exception occurred during the execution of the current web request... Exception Details: System.NotSupportedException: Collection is read-only.

  呀,隻讀……這是怎麼回事?不就是一個NameValueCollection嗎?在不得不請出.NET Reflector之後,老趙果然發現其中有存在某種問題或陰謀……

public class HttpRequest
{ 
    ...

    public NameValueCollection Form
    {
        get
        {
            if (this._form == null)
            {
                this._form = new HttpValueCollection();
                if (this._wr != null)
                {
                    this.FillInFormCollection();
                }
                this._form.MakeReadOnly();
            }
            if (this._flags[2])
            {
                this._flags.Clear(2);
                ValidateNameValueCollection(this._form, "Request.Form");
            }
            return this._form;
        }
    }

    ...
}
      

  雖然HttpRequest.Form屬性為NameValueCollection類型,但是其中的_form變量事實上是一個HttpValueCollection對象。而HttpValueCollection自然是NameValueCollection的子類,而造成其“隻讀”的最大原因便是:

[Serializable]
internal class HttpValueCollection : NameValueCollection
{ 
    ...

    internal void MakeReadOnly()
    {
        base.IsReadOnly = true;
    } 

    ...
}
      

  IsReadOnly是定義在NameValueCollection基類NameObjectCollectionBase上的protected屬性,這意味着如果我們隻有編寫一個如同NameValueCollection或HttpValueCollection般的子類才能直接通路它,而現在……反射吧,兄弟們。

public class FilterForbiddenWordModule : IHttpModule
{
    private static PropertyInfo s_isReadOnlyPropertyInfo;

    static FilterForbiddenWordModule()
    {
        Type type = typeof(NameObjectCollectionBase);
        s_isReadOnlyPropertyInfo = type.GetProperty(
            "IsReadOnly",
            BindingFlags.Instance | BindingFlags.NonPublic);
    }

    ...

    private static void ProcessCollection(NameValueCollection collection)
    {
        var copy = new NameValueCollection();

        foreach (string key in collection.AllKeys)
        {
            Array.ForEach(
                collection.GetValues(key),
                v => copy.Add(key, ForbiddenWord.Filter(v)));
        }

        // set readonly to false.
        s_isReadOnlyPropertyInfo.SetValue(collection, false, null);

        collection.Clear();
        collection.Add(copy);

        // set readonly to true.
        s_isReadOnlyPropertyInfo.SetValue(collection, true, null);
    }   
}      

  現在再打開個頁面看看,似乎沒事。那麼就來體驗一下這個HttpModule的功效吧。我們先準備一個空的aspx頁面,加上以下代碼:

<form id="form1" runat="server">
    <asp:TextBox runat="server" TextMode="MultiLine" />
    <asp:Button runat="server" Text="Click" />
</form>
      

  打開頁面,在文本框内填寫一些敏感字元并點選按鈕:

一個較完整的關鍵字過濾解決方案(上)

  嗨,效果似乎還不錯!

問題來了

  太簡單了,是不?

  可惜問題才剛開始:如果業務中有些字段不應該被替換怎麼辦?例如“密碼”。如果我們隻做到現在這點,那麼密碼“let-us-say-shit”和“let-us-say-fuck”則會被認為相同——伺服器端邏輯接收到的都是“let-us-say-**”。也就是說,我們必須提供一個機制,讓上面的HttpModule可以“忽略”掉某些内容。

  如果是其他一些解決方案,我們可以在用戶端進行一些特殊标記。例如在用戶端增加一個“-noffw-password”字段來表示忽略對“password”字段的過濾。不過根據著名的“Don't trust the client”原則,這種做法應該是第一個被否決掉的。試想,如果某些哥們發現了這一點(别說“不可能”),那麼想要繞開這種過濾方式實在是一件非常容易的事情。不過我們應該可以把這種“約定”直接運用在字段名上。例如原本我們如果取名為“password”的字段,現在直接使用“-noffw-password”,而HttpModule發現了這種字首就會放它一馬。由于字段的命名完全是由伺服器端決定,是以采取這種方式之後用戶端的惡人們就無法繞開我們的過濾了。

  還有一種情況就是我們要對某些特定的字段采取一些特殊的過濾方式。例如,之前相當長的一段時間内我認為在伺服器端反序列化一段JSON字元串是非常不合理的,不過由于AJAX幾乎成了事實标準,亦或是現在的Web應用程式經常需要傳遞一些結構複雜的對象,JSON格式已經越來越多地被伺服器端所接受。假如一個字段是表示一個JSON字元串,那麼首先我們隻應該對它的“值”進行過濾,而忽略其中的“鍵”。對于這種字段,我們依舊可以使用如上的命名約定來進行忽略。例如,我們可以使用-json-data的方法來告訴伺服器端這個字段應該被當作JSON格式進行處理。