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 語法不強制要求字串一定要用單引號包括起來,就懶得打了。此外,還有其他的好處列於下:
- Parament 可被其他 class 使用 (成為其他 class 的成員) ,做為其他 class 和資料庫表格 Example 互動時的端口。
- 嚴格的 DataType 檢查。
- 程式碼具有自我描述性,在程式碼中,就可以看出 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 的方式立即查驗輸入資料的正確性。
樂多舊回應