SQL 注入、XSS、指令注入是 Web 應用最常見的安全威脅。在 ASP.NET Web Forms 中,透過 `Global.asax` 的 `Application_BeginRequest` 事件攔截**所有進入請求**,統一驗證 QueryString、Form、Cookie 參數,配合 `Web.config` 設定驗證規則,可建立「零侵入、全站覆蓋、易維護」的防護層。本文提供完整實作方案。
<configuration>
<configSections>
<section name="parameterValidation" type="System.Configuration.NameValueSectionHandler" />
</configSections>
<parameterValidation>
<!-- 格式: 參數名稱 = 型別[:正則/最大長度/範圍] -->
<add key="id" value="int32" />
<add key="page" value="int32:1-1000" />
<add key="email" value="email" />
<add key="username" value="string:1-50:^[a-zA-Z0-9_]+$" />
<add key="search" value="string:0-100" />
<add key="orderId" value="guid" />
<add key="amount" value="decimal:0-999999.99" />
<add key="date" value="date:yyyy-MM-dd" />
</parameterValidation>
</configuration>
using System;
using System.Collections.Specialized;
using System.Configuration;
using System.Text.RegularExpressions;
using System.Web;
public static class ParameterValidator
{
private static readonly NameValueCollection _rules =
(NameValueCollection)ConfigurationManager.GetSection("parameterValidation");
/// 驗證單一參數值
public static bool Validate(string paramName, string paramValue, out string errorMessage)
{
errorMessage = null;
if (string.IsNullOrEmpty(paramValue)) return true; // 空值視為通過 (必要性由業務邏輯決定)
if (_rules == null || _rules[paramName] == null)
return true; // 未設定規則的參數預設放行 (或可改為封鎖)
string rule = _rules[paramName];
string[] parts = rule.Split(':');
string type = parts[0].ToLower();
try
{
switch (type)
{
case "int32":
if (!int.TryParse(paramValue, out int intVal)) return Fail("非整數");
if (parts.Length > 1 && !CheckRange(intVal, parts[1])) return Fail("超出範圍");
break;
case "int64":
if (!long.TryParse(paramValue, out long longVal)) return Fail("非長整數");
if (parts.Length > 1 && !CheckRange(longVal, parts[1])) return Fail("超出範圍");
break;
case "decimal":
if (!decimal.TryParse(paramValue, out decimal decVal)) return Fail("非數值");
if (parts.Length > 1 && !CheckRange(decVal, parts[1])) return Fail("超出範圍");
break;
case "guid":
if (!Guid.TryParse(paramValue, out _)) return Fail("非合法 GUID");
break;
case "date":
string format = parts.Length > 1 ? parts[1] : "yyyy-MM-dd";
if (!DateTime.TryParseExact(paramValue, format, null, System.Globalization.DateTimeStyles.None, out _))
return Fail($"日期格式錯誤,應為 {format}");
break;
case "email":
if (!Regex.IsMatch(paramValue, @"^[\w\.-]+@[\w\.-]+\.\w{2,}$")) return Fail("Email 格式錯誤");
break;
case "string":
int maxLen = 255;
string pattern = null;
if (parts.Length > 1)
{
// 格式: maxLen 或 maxLen:regex
string[] lenParts = parts[1].Split(':');
if (int.TryParse(lenParts[0], out int ml)) maxLen = ml;
if (lenParts.Length > 1) pattern = lenParts[1];
}
if (paramValue.Length > maxLen) return Fail($"長度超過 {maxLen}");
if (!string.IsNullOrEmpty(pattern) && !Regex.IsMatch(paramValue, pattern))
return Fail("格式不符合規則");
break;
default:
return true; // 未知型別放行
}
return true;
}
catch (Exception ex)
{
errorMessage = ex.Message;
return false;
}
bool Fail(string msg)
{
errorMessage = $"參數 [{paramName}] {msg}: {paramValue}";
return false;
}
}
private static bool CheckRange(T value, string rangeSpec) where T : IComparable
{
// 格式: min-max
string[] bounds = rangeSpec.Split('-');
if (bounds.Length != 2) return true;
if (T.TryParse(bounds[0], out T min) && value.CompareTo(min) < 0) return false;
if (T.TryParse(bounds[1], out T max) && value.CompareTo(max) > 0) return false;
return true;
}
/// 驗證 HttpRequest 所有輸入集合
public static bool ValidateRequest(HttpRequest request, out string errorMessage)
{
errorMessage = null;
// 合併 QueryString、Form、Cookies (可擴充 Headers)
var collections = new NameValueCollection[] { request.QueryString, request.Form, request.Cookies };
foreach (var coll in collections)
{
foreach (string key in coll)
{
if (string.IsNullOrEmpty(key)) continue;
string value = coll[key];
if (!Validate(key, value, out errorMessage))
return false;
}
}
return true;
}
}
public class Global : System.Web.HttpApplication
{
protected void Application_BeginRequest(object sender, EventArgs e)
{
// 可選:排除靜態資源、特定路徑
string path = Request.Path.ToLower();
if (path.EndsWith(".css") || path.EndsWith(".js") || path.EndsWith(".png") ||
path.EndsWith(".jpg") || path.EndsWith(".ico") || path.Contains("/api/public/"))
return;
if (!ParameterValidator.ValidateRequest(Request, out string errorMsg))
{
// 記錄攻擊日誌
LogAttack(Request, errorMsg);
// 導向錯誤頁 (避免回傳 500 洩露資訊)
Response.Clear();
Response.StatusCode = 400;
Server.Transfer("~/Security/ParameterError.aspx?msg=" + HttpUtility.UrlEncode(errorMsg));
Response.End();
}
}
private void LogAttack(HttpRequest request, string errorMsg)
{
var log = new
{
Time = DateTime.Now,
IP = request.UserHostAddress,
Url = request.RawUrl,
Method = request.HttpMethod,
UserAgent = request.UserAgent,
Referrer = request.UrlReferrer?.ToString(),
Error = errorMsg,
Params = GetSafeParams(request) // 避免記錄敏感欄位 (密碼、Token)
};
// 寫入資料庫、檔案、ELK、Application Insights 等
System.Diagnostics.Trace.WriteLine($"[SECURITY] {Newtonsoft.Json.JsonConvert.SerializeObject(log)}");
}
private string GetSafeParams(HttpRequest request)
{
var safe = new NameValueCollection();
string[] sensitive = { "password", "pwd", "token", "secret", "key", "auth", "credential" };
foreach (string key in request.Params)
{
if (!Array.Exists(sensitive, s => key.ToLower().Contains(s)))
safe[key] = request.Params[key];
}
return Newtonsoft.Json.JsonConvert.SerializeObject(safe);
}
}
<%@ Page Language="C#" %>
<!DOCTYPE html>
<html><head><title>參數驗證失敗</title></head><body>
<h2>請求參數不合法</h2>
<p>系統偵測到異常參數,請求已被攔截。</p>
<p><strong>詳細資訊:</strong> <%= Server.HtmlEncode(Request.QueryString["msg"]) %></p>
<p><a href="javascript:history.back()">返回上一頁</a></p>
</body></html>
| 策略 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 白名單 (本方案) | 安全性高、精確控制、誤報低 | 新增參數需修改設定、初期維護成本高 | 企業級應用、金融/醫療/政府系統 |
| 黑名單 (關鍵字過濾) | 部署快、無需改代碼 | 繞過技巧多、誤攔正常輸入 (如 "select * from user") | 緊急臨時防護、舊系統無法重構時 |
| WAF (Web Application Firewall) | 零代碼變更、規則庫自動更新、防禦 0-day | 成本高、可能誤攔、需調校 | 生產環境前置防護 (Azure WAF, AWS WAF, Cloudflare, ModSecurity) |
// Program.cs
app.Use(async (context, next) =>
{
// 同樣邏輯驗證 Query、Form、Body
if (!ValidateParameters(context.Request))
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("Invalid parameters");
return;
}
await next();
});
// 或使用 FluentValidation + 自動驗證管線
builder.Services.AddControllers(options =>
options.Filters.Add());
結論: `Global.asax` 統一參數驗證是 ASP.NET Web Forms 時代最實用的「零侵入」防注入方案。核心在於**白名單型別驗證 + 設定外部化 + 完整稽核日誌**。現代專案請遷移至 ASP.NET Core Middleware + FluentValidation + WAF 多層防護體系。