提供 ERP 查詢時,很容易遇到一個期待:能不能讓呼叫端自己組條件?我能理解這個需要,每個組合都開一支 API,後面會有很多重複工作。不過,實際送往 ERP 的內容,還是要由平台掌握。
這一篇想整理的,就是先解析條件、確認白名單,再由平台重新組出下游查詢的做法。
force),呼叫端不能移除或放寬。直接把呼叫端字串接進 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')——要同時成立才回傳,結果必然是空的。他不會收到錯誤,也不會拿到資料。
取回大量結果後,還要逐筆過濾,會是一筆負擔。所以我會希望在送出查詢前,就把資料範圍限制好,讓下游只回應允許的資料。
述詞 (AND 述詞)*,述詞為 欄位 運算子 值。運算子限 =、!=、<、<=、>、>=、LIKE、IN(IN 清單只接受單引號字串值,如 IN ('A','B'),數字清單不支援);不支援 OR、UNION、子查詢、註解與 multi-statement。' 跳脫為 '';純數值不加引號(文法已限制只含數字與小數點)。這一步擋的是藏在值裡的攻擊字串——它在第 1 步完全合法,跳脫讓它送到 ERP 時仍然只是一個字串。AND 上 force。呼叫端原字串只用於解析意圖,不直接送往下游。
圖 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 與非同步事件。