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