iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Software Development

30 天把舊 ERP 整合成現代微服務平台:架構治理與工程實作雙軌實戰系列 第 16 篇

Day 16|condition 白名單:查詢保有彈性,呼叫端仍不能傳 SQL

  • 分享至 

  • xImage
  •  

提供 ERP 查詢時,很容易遇到一個期待:能不能讓呼叫端自己組條件?我能理解這個需要,每個組合都開一支 API,後面會有很多重複工作。不過,實際送往 ERP 的內容,還是要由平台掌握。

這一篇想整理的,就是先解析條件、確認白名單,再由平台重新組出下游查詢的做法。

本篇名詞小筆記

  • Operation:平台對外開放的一個查詢入口,對應 ERP 端一支既有服務。SQL 寫在 ERP 端,平台只檢查呼叫端帶來的條件能不能拼進去。
  • 白名單:只允許事先核准的欄位、運算子與資料表,其餘條件一律拒絕。
  • SQL Injection:SQL 注入,攻擊者透過輸入內容改變原本查詢語意,執行未授權的資料庫操作。
  • Force Condition:平台強制附加的查詢條件(設定檔裡的 force),呼叫端不能移除或放寬。
  • Allow Empty:是否允許不帶查詢條件的設定;涉及大量資料時通常應預設拒絕。

今天要解決的問題

直接把呼叫端字串接進 WHERE,會讓可執行的查詢很難控制;完全不提供組合能力,又可能累積很多相近端點。我會先把允許表達的條件訂清楚,讓彈性有一個明確範圍。

我的想法是:呼叫端描述想查什麼,平台負責確認能不能查,以及最後送出去的內容。兩邊的工作先分清楚。

架構師視角:白名單模型與資料範圍收斂

先替每張允許查詢的表準備一份 YAML,把可用欄位與運算子列出來,並放進版本控管。之後要開放什麼,就有一份可以對照的清單。

table: item_master
columns:
  item_no:     { ops: [=, LIKE, IN] }     # 料號(主鍵)
  item_name:   { ops: [=, LIKE] }         # 品名規格
  item_group:  { ops: [=, IN] }           # 品號群組
  item_type:   { ops: [=, IN] }           # 料件性質
  stock_unit:  { ops: [=] }               # 庫存單位
  active_flag: { ops: [=] }               # 有效碼
defaults:
  force: "active_flag = 'Y'"   # 強制附加,呼叫端不可關閉
  allowEmpty: false            # 空條件在 ERP 端本即報錯,故 fail-closed 回 400

force 放一定要附加的條件,allowEmpty 決定空條件能不能通過。沒有白名單的表,先拒絕。這幾個基本原則,我會先確認每個人都理解一致。

呼叫端可見的資料範圍,同樣在重組階段以 欄位 IN ('v1', 'v2') 附加,與 force 走同一條軌道——額外 AND 上去,不受呼叫端條件影響。允許值為空時直接拒絕,不會組出無限制的條件。

假設某個呼叫端只能看到 FG 與 SP 兩個品號群組,重組時就會附加 item_group IN ('FG','SP')。他若送 item_group='RM' 想查別的群組,重組後這兩個條件會一起出現——item_group = 'RM' AND item_group IN ('FG','SP')——要同時成立才回傳,結果必然是空的。他不會收到錯誤,也不會拿到資料。

取回大量結果後,還要逐筆過濾,會是一筆負擔。所以我會希望在送出查詢前,就把資料範圍限制好,讓下游只回應允許的資料。

工程師視角:四步淨化與稽核

  1. 解析:受限文法 述詞 (AND 述詞)*,述詞為 欄位 運算子 值。運算子限 =、!=、<、<=、>、>=、LIKE、IN(IN 清單只接受單引號字串值,如 IN ('A','B'),數字清單不支援);不支援 OR、UNION、子查詢、註解與 multi-statement。
  2. 比對:每個述詞的欄位與運算子都須在該表白名單內,否則拒絕。
  3. 跳脫:字串值以單引號包裹,內部 ' 跳脫為 '';純數值不加引號(文法已限制只含數字與小數點)。這一步擋的是藏在值裡的攻擊字串——它在第 1 步完全合法,跳脫讓它送到 ERP 時仍然只是一個字串。
  4. 重組:由平台重新組出條件字串,再 AND 上 force。呼叫端原字串只用於解析意圖,不直接送往下游。

https://ithelp.ithome.com.tw/upload/images/20260925/20184230OvfhZ40xSL.png

圖 Day 16-1:condition 白名單與條件重組。

四步實際跑起來長什麼樣,拿前面那份 item_master 白名單走一次。先看下游:ERP 端的查詢是既有的,平台不產生也不修改,只負責把條件字串交到那個插入點。

SELECT item_no, item_name, stock_unit
  FROM item_master
 WHERE <condition>

呼叫端想查品號群組 FG、品名含「感測器」的料件,送來這段:

item_group='FG' AND item_name LIKE '%感測器%'

兩個述詞的欄位與運算子都在白名單內,通過比對。平台重組後交出去的是這段——最後那個 active_flag 是 force 加上去的,呼叫端沒送也關不掉:

item_group = 'FG' AND item_name LIKE '%感測器%' AND active_flag = 'Y'

換成注入嘗試。下面這段的 OR 不在受限文法裡,所以解析階段就失敗——停在第一步,回 400,上面那句 SELECT 完全沒有被執行:

item_no='A001' OR '1'='1'

值得對照的是,如果沒有這層解析、把字串直接接進 WHERE,同一段輸入會變成下面第一行。關鍵在運算順序:AND 會先算,資料庫實際讀到的是第二行那種分組。

 WHERE item_no='A001' OR '1'='1' AND active_flag = 'Y'
-- AND 先算,等於:
 WHERE item_no='A001' OR ('1'='1' AND active_flag = 'Y')

OR 的意思是「左邊成立或右邊成立,這筆就撈出來」。右邊那組的 '1'='1' 永遠成立,等於沒有這個條件,所以右邊只剩下 active_flag = 'Y'——整句變成「料號等於 A001,或者有效碼等於 Y」。

稽核要看到的是實際送出的內容,注入字串也不該進入 Log。這兩件事用同一個做法就能同時達成:記錄重組後的條件,而不是原始輸入。每次查詢留下 operation、呼叫者、重組後條件、耗時、筆數與結果狀態。

錯誤訊息使用固定描述,不把呼叫端送來的字串原封不動放進回應。白名單要修改,也和程式一樣經過 Review、測試與部署,讓後續查得到當時開放了哪些條件;白名單也能由外部掛載目錄覆寫,這條路徑同樣要走 Review,不能變成繞過版控的後門。

策略取捨與限制

取捨 這樣選的理由 何時要重新評估
一個 operation 只對應一份白名單 設定可審查、行為可預期 同一條件需拼進不同表時
未配白名單的表一律拒絕 漏設時失敗方向是安全的 無
force 逐表判斷是否需要 下游已有可靠前置過濾就不必重複 過濾改為後置或可被截斷時
錯誤訊息不帶回原始輸入 避免注入 payload 進入 Log 與回應 無;除錯需求應由受控稽核紀錄滿足

同一條件需拼進不同表時,只能把多張表的欄位合併成同一份清單。這個代價要事先說明:一個 operation 若涵蓋多張表的欄位,呼叫端用錯欄位時會得到下游錯誤而非 400,錯誤訊息的可讀性會下降。

條件通過白名單,查詢速度還是要另外確認。我會一起看執行計畫、索引、筆數上限與 timeout,避免條件雖然在允許範圍內,卻讓 ERP 等待很久。

驗證方式與衡量指標

測試至少涵蓋以下情境,每一項都應確認實際送往下游的字串:

測試情境 預期行為
引號 breakout(如 ' OR '1'='1) 400,且不產生任何下游查詢
OR、UNION、註解、multi-statement 400(文法不支援)
未允許欄位 400
未允許運算子 400
空條件且 allowEmpty: false 400
未配白名單的表 400
資料範圍允許值為空 400,不得組出無範圍限制的條件

只看回應碼,還不能證明檢查確實發生在送出之前。所以測試被拒絕的條件時,我還會確認下游完全沒有收到查詢。

今天先整理到這裡

今天整理的做法,是先把能查的範圍列清楚,再確認平台實際送出了什麼。這兩件事一起做好,查詢才比較容易管理與追查。下一篇,再來比較同步 API 與非同步事件。

參考資料

  1. OWASP, OWASP SQL Injection Prevention,查閱日期:2026-09-29。

上一篇
Day 15|REST 與 SOAP 轉換:不只是 XML 改成 JSON
下一篇
Day 17|REST 還是 MQ?先問業務是否需要立即答案
系列文
30 天把舊 ERP 整合成現代微服務平台:架構治理與工程實作雙軌實戰 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言