[安全測試] 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 看程式碼內部,接下來要學怎麼從外部攻擊自己的系統,找出只有在運行時才會出現的漏洞。


延伸閱讀