排序攻擊面-ORDER-BY注入與字段白名單)
動態(tài)排序攻擊面ORDER BY 注入與字段白名單攻擊面查詢值可以使用預(yù)編譯參數(shù)表名和排序列卻不能直接用?占位。為了支持前端表格排序不少項(xiàng)目把sortField原樣拼進(jìn)${}由此留下 SQL 注入、越權(quán)探測和不可控慢查詢風(fēng)險(xiǎn)。本文給出枚舉白名單、字段映射和類型安全方法引用三層方案并結(jié)合 MetaLiteOrderBy的源碼說明公共查詢組件應(yīng)該負(fù)責(zé)什么、不能相信什么。很多后臺表格都會提交這兩個參數(shù){sortField:createdTime,sortDirection:desc}為了生成動態(tài)排序一些 MyBatis XML 會寫成ORDER BY ${sortField} ${sortDirection}功能上線很快但${}是字符串替換不會像#{}一樣變成預(yù)編譯參數(shù)。只要字段來自外部輸入攻擊者就可能改變 SQL 結(jié)構(gòu)。一、為什么排序列不能使用普通占位符下面這種寫法通常不能表達(dá)動態(tài)列名ORDERBY?DESC數(shù)據(jù)庫會把?當(dāng)成一個值而不是 SQL 標(biāo)識符。列名、表名、排序方向?qū)儆?SQL 結(jié)構(gòu)必須在生成 SQL 前完成可信映射。這也是動態(tài)排序比普通查詢參數(shù)更危險(xiǎn)的原因user_id ?的值不會改變 SQL 結(jié)構(gòu)ORDER BY ${field}會直接改變解析后的語句。二、風(fēng)險(xiǎn)不只有傳統(tǒng) SQL 注入即使數(shù)據(jù)庫驅(qū)動禁止多語句執(zhí)行動態(tài)排序仍可能帶來其他問題。1. 探測內(nèi)部字段攻擊者不斷嘗試字段名可以推斷表結(jié)構(gòu)、隱藏列或數(shù)據(jù)庫函數(shù)是否可用。2. 構(gòu)造高成本表達(dá)式復(fù)雜函數(shù)、計(jì)算表達(dá)式或未建索引字段可能讓分頁查詢突然變慢。3. 繞過數(shù)據(jù)展示規(guī)則某些字段雖然允許查詢卻不應(yīng)該成為公開排序條件。例如內(nèi)部風(fēng)險(xiǎn)分、邏輯刪除標(biāo)記或安全等級。4. 排序結(jié)果不穩(wěn)定只按非唯一字段排序翻頁時(shí)可能出現(xiàn)重復(fù)或遺漏記錄。三、最穩(wěn)妥的入口是業(yè)務(wù)白名單前端參數(shù)不應(yīng)直接等于數(shù)據(jù)庫字段而應(yīng)先映射到服務(wù)端枚舉enumUserSortField{CREATED_TIME,USERNAME,STATUS}Service 再把枚舉轉(zhuǎn)換為受信字段OrderByorderByswitch(param.getSortField()){caseCREATED_TIME-OrderBy.desc(UserEntity::getCreatedTime);caseUSERNAME-OrderBy.asc(UserEntity::getUsername);caseSTATUS-OrderBy.asc(UserEntity::getStatus);};這樣前端只能選擇產(chǎn)品允許的排序能力無法提交任意數(shù)據(jù)庫標(biāo)識符。四、MetaLite 如何把字段引用轉(zhuǎn)換成列名MetaLiteOrderBy支持字符串字段OrderBy.desc(createdTime)也支持 Java 方法引用OrderBy.desc(UserEntity::getCreatedTime)方法引用版本通過EntityHelper.genFieldName(function)提取實(shí)體屬性publicstaticT,ROrderBydesc(EntityFieldNameFunctionT,Rfunction){AssertUtil.paramNotNull(function,function);returnnewOrderBy(EntityHelper.genFieldName(function),Direction.DESC);}JDBC SQL 生成階段再從實(shí)體屬性到數(shù)據(jù)庫列名映射表中取值sb.append(property2ColumnMap.get(orderBy.getKey())).append( ).append(orderBy.getDirection());這里體現(xiàn)了兩層約束排序方向來自框架枚舉只能是受支持的值方法引用必須指向?qū)嶓w真實(shí)屬性字段改名時(shí)編譯器能夠發(fā)現(xiàn)問題。五、方法引用不等于前端輸入已經(jīng)安全如果 Controller 仍然接收任意字符串然后調(diào)用OrderBy.desc(param.getSortField())雖然property2ColumnMap不一定能映射非法字段但這不是完整的安全契約。調(diào)用方仍應(yīng)校驗(yàn)字段是否在當(dāng)前接口白名單排序方向是否合法字段是否適合建立排序索引是否需要追加穩(wěn)定的主鍵排序。公共 ORM 可以提供類型安全能力卻無法判斷某個字段是否應(yīng)該開放給某個頁面。六、分頁排序必須增加唯一兜底字段假設(shè)大量訂單的創(chuàng)建時(shí)間完全相同ORDERBYcreated_timeDESCLIMIT20OFFSET20數(shù)據(jù)庫對相同時(shí)間記錄的內(nèi)部順序沒有承諾。并發(fā)插入后第二頁可能重復(fù)出現(xiàn)第一頁數(shù)據(jù)也可能漏掉記錄。建議寫成query.orderBy(OrderBy.desc(OrderEntity::getCreatedTime),OrderBy.desc(OrderEntity::getId));其中id負(fù)責(zé)形成確定的全序。七、復(fù)雜排序應(yīng)該怎樣處理真實(shí)業(yè)務(wù)可能需要聚合別名排序距離排序自定義狀態(tài)優(yōu)先級跨表字段排序數(shù)據(jù)庫函數(shù)計(jì)算排序。這類排序不應(yīng)為了復(fù)用通用列表接口而開放任意表達(dá)式。更合適的做法是為場景定義專用查詢方法SQL 表達(dá)式固定在服務(wù)端外部只傳有限的策略枚舉對慢排序建立獨(dú)立索引和執(zhí)行計(jì)劃測試。MetaLite 的字符串OrderBy是擴(kuò)展逃生口不應(yīng)該成為外部參數(shù)直通 SQL 的入口。八、可直接復(fù)用的動態(tài)排序檢查清單Controller 不直接接收數(shù)據(jù)庫列名外部字段先映射為服務(wù)端枚舉排序方向只允許ASC、DESCORM 內(nèi)優(yōu)先使用實(shí)體方法引用分頁排序追加唯一主鍵公開排序字段有索引和慢 SQL 驗(yàn)證聚合、函數(shù)和跨表排序使用專用查詢?nèi)罩居涗涀罱K排序策略但不回顯內(nèi)部表結(jié)構(gòu)。九、結(jié)論動態(tài)排序的本質(zhì)不是“拼一個 ORDER BY”而是把外部意圖轉(zhuǎn)換為受控的 SQL 結(jié)構(gòu)。MetaLite 方法引用解決了字段改名和內(nèi)部類型安全問題業(yè)務(wù)白名單解決了哪些能力允許對外開放的問題穩(wěn)定排序和索引驗(yàn)證解決了分頁正確性與性能問題。三層缺一不可??蚣芎喗镸etaLite 是面向企業(yè)生產(chǎn)環(huán)境的新一代 Java 微服務(wù)技術(shù)底座。系列文章重點(diǎn)分享代碼背后的設(shè)計(jì)思路、技術(shù)取舍與工程實(shí)踐。源碼基線JDK 21、Spring Boot 3.2.9、Spring Cloud 2023.0.1、Spring Cloud Alibaba 2023.0.1.3具體組件版本以項(xiàng)目backend-bom為準(zhǔn)。作者簡介15 年 Spring 體系企業(yè)級開發(fā)經(jīng)驗(yàn)專注于 Java 微服務(wù)架構(gòu)、工程治理與生產(chǎn)實(shí)踐。持續(xù)更新MetaLite 系列內(nèi)容將持續(xù)更新圍繞核心設(shè)計(jì)、源碼鏈路、技術(shù)取舍與生產(chǎn)實(shí)踐展開。歡迎關(guān)注作者及時(shí)獲取后續(xù)內(nèi)容。在線演示演示地址: https://admin.metalite.top/演示賬號: guess演示密碼: admin2026