最近更新: 2006-06-26

ASP.Net~~SqlParameter 和 Ad-Hoc 該用哪種方法?

回應「對SqlParameter的不求甚解」一文。SqlParameter 和 Ad-Hoc 這兩種用法的選擇,嚴格來說是 programmer 的態度與技術問題,而不是安全性問題。我的習慣是 Ad-Hoc ,但我同時強調,資料的驗證與查核是 programmer 的份內工作,所以我的程式風格如下。試問這種 Ad-Hoc 寫法的安全性,會比 SqlParameter 差嗎?

安全的 Ad-Hoc 程式風格

class Parament {
    private string xUserID;
    public string UserID {
        get { return xUserID; }
        set {
            if(value != ""
             && Regex.IsMatch(value, @"^[a-z][a-z0-9]{2,31}$", RegexOptions.IgnoreCase) == false) {
                throw new System.ArgumentException();
            }
            else {
                xUserID = value;
            }
        }
    }

    private int xQty;
    public int Qty {
        get { return xQty; }
        set {
            if(value < 0 || value > 1000) {
                throw new System.ArgumentException();
            }
            else {
                xQty = value;
            }
        }
    }

    private string xMemo;
    public string Memo {
        get { return xMemo; }
        set {
            if(value.Length > 500) {
                throw new System.ArgumentException();
            }
            else {
                xMemo = value;
            }
        }
    }

    private DateTime xModifyTime;
    public DateTime ModifyTime {
        get { return xModifyTime; }
        set {
            xModifyTime = value;
        }
    }
}

static public string SqlEscapeString(string s) {
    return Regex.Replace(s, @"'", "''");
}

Parament parament = new Parament();

parament.UserID = "ABC123";
parament.Qty = 100;
parament.ModifyTime = DateTime.Now;

parament.Memo = "許功蓋abc'xyz--;select";
// 如果在 Ad-Hoc 時,省略單引號,那麼這一行可 inject 。


string SqlCommand = String.Format("INSERT INTO Example (UserID, Qty, Memo, ModifyTime) VALUES ( '{0}', {1}, '{2}', '{3}')", parament.UserID, parament.Qty, SqlEscapeString(parament.Memo), parament.ModifyTime.ToString("s"));

/*
string SqlCommand = String.Format("INSERT INTO Example (UserID, Qty, Memo, ModifyTime) VALUES ( '{0}', {1}, '{2}', CAST('{3}' AS DATETIME))", parament.UserID, parament.Qty, SqlEscapeString(parament.Memo), parament.ModifyTime.ToString("s"));
*/

這是我在用 PHP 時就養成的習慣,透過 C# 的 Property 語法,程式碼更簡潔了,但觀念仍然不變。這種程式風格,也可以用 SqlParameter 實現,這稍候再提。請注意到我對 Memo 的處理方式,很多 programmer 不是漏掉 escaping ,就是在 SQL 命令中,省略了括住字串的單引號,這就是 programmer 的態度問題:因為 SQL 語法不強制要求字串一定要用單引號包括起來,就懶得打了。此外,還有其他的好處列於下:

  1. Parament 可被其他 class 使用 (成為其他 class 的成員) ,做為其他 class 和資料庫表格 Example 互動時的端口。
  2. 嚴格的 DataType 檢查。
  3. 程式碼具有自我描述性,在程式碼中,就可以看出 UserID 的資料格式是「英文字母開頭,中間可混用英、數字,最短3字元,最長32字元」, Qty 的允許值範圍是 0 ~ 1000 。

使用 SqlParameter

class Parament {
    // Bacause SqlCommand is sealed, you can use it but cannot inherit.
    private SqlCommand sqlCommand;
    public SqlCommand Command {
        get { return sqlCommand; }
    }

    public Parament() {
        sqlCommand = new SqlCommand();
        sqlCommand.Parameters.Add("@UserID", SqlDbType.VarChar, 32);
        sqlCommand.Parameters.Add("@Qty", SqlDbType.Int);
        sqlCommand.Parameters.Add("@Memo", SqlDbType.VarChar, 500);
        sqlCommand.Parameters.Add("@ModifyTime", SqlDbType.DateTime);
    }

    public string UserID {
        get { return Convert.ToString(sqlCommand.Parameters["@UserID"].Value); }
        set {
            if(value != ""
                && Regex.IsMatch(value, @"^[a-z][a-z0-9]{2,31}$", RegexOptions.IgnoreCase) == false) {
                throw new System.ArgumentException();
            }
            else {
                sqlCommand.Parameters["@UserID"].Value = value;
            }
        }
    }

    public int Qty {
        get { return Convert.ToInt32(sqlCommand.Parameters["@Qty"].Value); }
        set {
            if(value < 0 || value > 1000) {
                throw new System.ArgumentException();
            }
            else {
                sqlCommand.Parameters["@Qty"].Value = value;
            }
        }
    }

    public string Memo {
        get { return Convert.ToString(sqlCommand.Parameters["@Memo"].Value); }
        set {
            if(value.Length > 500) {
                throw new System.ArgumentException();
            }
            else {
                sqlCommand.Parameters["@Memo"].Value = value;
            }
        }
    }

    public DateTime ModifyTime {
        get { return Convert.ToDateTime(sqlCommand.Parameters["@ModifyTime"].Value); }
        set {
            sqlCommand.Parameters["@ModifyTime"].Value = value;
        }
    }
}

Parament parament = new Parament();

parament.UserID = "ABC123";
parament.Qty = 100;
parament.ModifyTime = DateTime.Now;

parament.Memo = "許功蓋abc'xyz--;select";

parament.Command.CommandText = "INSERT INTO Example (UserID, Qty, Memo, ModifyTime) VALUES ( '@UserID', @Qty, '@Memo', '@ModifyTime');";

同樣的程式風格,也可像上面這樣用 SqlCommand 來實作。而這兩種用法外觀上的主要差別,在於少了一個 escaping 的動作。以我的觀點,這個工作本來就是 programmer 應該要加的,不能因為常常有 programmer 忘了加 escaping ,而斷定 Ad-Hoc 的安全性較差。因為這是 programmer 的態度問題。追求簡潔的程式碼,和鄉愿地假定輸入的資料都是「善意的」而省略該做的事,是兩碼子事。透過對同一程式風格的兩種不同實現方式,應該更容易了解 SqlParameter 的實作目的。只是我向來對 M$ 感冒的原因之一也在此,為什麼它就不能說清楚為什麼要用 SqlParameter 呢?把 programmer 當笨蛋一樣,好像在說「你用就對了,不需要知道理由」。附帶一提,因為 SqlParameter 不查驗資料格式,而對資料型態的核對則會等到進行查詢時才進行,所以依然應該使用 Property 的方式立即查驗輸入資料的正確性。

樂多舊網址: http://blog.roodo.com/rocksaying/archives/1816510.html

樂多舊回應
xoran_tc@yahoo.com(Tony Chu) (#comment-16464301)
Tue, 20 May 2008 03:10:02 +0800
我的中文不是很好,所以如有文法上的錯誤, 請見諒.

你說的的確很有道理. 如果我只會用微軟給我的工具, 而我不知道這些工具到底掩蔽了哪些問題, 這對於我寫程式的[功力]的確有所影響. 但是對於微軟這種[語言簡化]的作風, 我想還是有一個好處的.

這個好處是什麼呢? 如果明天SQL這個語言的 escaping rules 改了. 如果你是用微軟[正統]的方法 of using SqlParameters 的話, 你大可以放心的等, 因為你知道微軟如果把這些新的 rules 放到 .NET 裡面的話, 它一定會同時改變 SqlParameter 確定它不受到這個 rule 的影響. 但是如果你是用你自己的 escaping 的方法的話, 那你什麼時候需要upgrade或是改你的程式時, 你就得確定你的 escape rules 還能100%的用.

當然啦, 無論如何測試都是必要的, 但是我想使用微軟建議的方法還是比較能確保 compatibility 吧...

-Tony
未留名 (#comment-16465019)
Tue, 20 May 2008 09:23:47 +0800
如果你熟悉 SQL 語法,那麼你說的疑慮不復存在。因為 SQL standard syntax 有很明確的 escaping rule. 這個 rule 比微軟 .Net API 還穩定不變。

在我的經驗中, API 是易變的,syntax 卻是穩定的。為何你相信易變的 Framework API,卻不相信幾乎不變的語法呢?

當我產生 SQL 查詢句時,我用了 SQL standard syntax 抽離這個句子對資料庫系統的相依性。這不因 upgrade 而改變。