[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 |