[安全測試] 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 標籤中:
- 輸入目標 URL:<code>http://localhost:3000</code>
- 點擊「Attack」
- 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 不是完美的,它有時候會報告「假警報」。處理誤報的步驟:
- 手動驗證:對 ZAP 標記的漏洞,嘗試手動重現攻擊
- 分析上下文:看看這個 Alert 在你的系統中是否真的構成風險
- 標記為誤報:在 ZAP 中將確認的誤報標記為「False Positive」,避免下次重複出現
- 記錄原因:記下為什麼判定為誤報,方便團隊日後參考
產出掃描報告:給團隊和主管看的安全報告
從 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 工具會一次全部標出來:
- 硬編碼密碼(<code>DB_PASSWORD</code> 直接寫在程式碼裡)
- SQL Injection(字串拼接組合 SQL 查詢)
- 錯誤資訊洩露(把資料庫錯誤訊息直接回傳給使用者)
- 過度資料暴露(回傳整個 <code>results</code> 物件,可能包含敏感欄位)
這就是 SAST 的價值——它不會累、不會忘記、不會因為趕 deadline 就放水。
三、工具選擇:SonarQube vs. Semgrep
市面上 SAST 工具百百種,但對於台灣的中小型開發團隊來說,有兩個工具特別值得認識:SonarQube 和 Semgrep。它們各有特色,適合不同的使用情境。
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 看程式碼內部,接下來要學怎麼從外部攻擊自己的系統,找出只有在運行時才會出現的漏洞。