[安全需求] 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:既有的舊專案怎麼補寫安全需求?

不要試圖一次補完所有功能的安全需求。建議的優先順序:

  1. 最先補:處理金流、個資的功能
  2. 其次補:對外暴露的 API、認證相關功能
  3. 再其次:有使用者輸入的功能
  4. 最後補:內部工具、管理後台

每次迭代時,順帶補上你正在修改的功能的安全需求,逐步累積。


九、結語:安全需求不是額外工作,是需求的完整表達

很多開發者把安全需求當成「額外的負擔」,但換個角度想——你寫的功能需求說「使用者可以登入」,卻沒說密碼怎麼存,這其實是需求不完整,不是安全問題。

安全需求規格書的本質,是讓需求從「能用」進化到「能安全地用」

就像建築師不會只畫出房間的位置而不標示防火通道和逃生路線,軟體工程師也不應該只定義功能而不定義防護。

從今天開始,每寫一條使用者故事,就多問自己一句:

「如果攻擊者看到這個功能,他會怎麼做?」

這個習慣,就是安全需求的起點。


延伸閱讀