解決mybatis-plus動態排序,導致的SQL注入問題
關於什麼是SQL注入,需要先自行通過搜索引擎瞭解
mybatis-plush提供的動態排序API
setOrderBy() 可以設置一個排序的語句,用於動態的排序
<code>PageHelper.startPage(1, 10).setOrderBy("`id` DESC");
List<fooentity> foos = this.fooMapper.foos();
foos.forEach(System.out::println);
/<fooentity>/<code>
- SQL日誌SELECT * FROM `foo` order by `id` DESC LIMIT ?
- 輸出的結果
<code>FooEntity [id=5, name=E]
FooEntity [id=4, name=D]
FooEntity [id=3, name=C]
FooEntity [id=2, name=B]
FooEntity [id=1, name=A]
/<code>
SQL注入問題
動態排序的 order by 語句,並不是預編譯的。而且多是由前端提交參數。可能存在SQL注入的問題
<code>// 惡意構造一條刪除的SQL語句
PageHelper.startPage(1, 10).setOrderBy("`id` DESC; DELETE FROM `foo` WHERE `id` = 1; SELECT 1 FROM `foo`");
List<fooentity> foos = this.fooMapper.foos();
foos.forEach(System.out::println);
/<fooentity>/<code>
SQL日誌
可以看到,執行檢索後,成功的刪除掉了一條記錄
<code>FooMapper.foos : ==> Preparing: SELECT * FROM `foo` order by `id` DESC; DELETE FROM `foo` WHERE `id` = 1; SELECT 1 FROM `foo` LIMIT ?
FooMapper.foos : ==> Parameters: 10(Integer)
FooMapper.foos : <== Total: 5
FooMapper.foos : <== Updates: 1
/<code>
解決辦法
通過搜索引擎可以發現很多解決方案。
使用枚舉固定排序的字段
就是把需要的排序策略,寫成枚舉類。根據前端提供的參數,來選擇枚舉。進行排序。這種方法可以避免由客戶端直接提交數據生成SQL。但是缺點就是不夠靈活。當字段更改的試試,需要修改代碼。
<code>enum Order {
\tNAME_ASC("`name` ASC"),
\tID_DESC("`id` DESC")
\t...
}
/<code>
使用正則過濾
其實排序字段,就是DB表的字段的規則,只能是:英文,數字,下劃線。非法的SQL運算符號並不符合這個規則,於是可以考慮通過正則來判斷排序字段是否合法。
先假定客戶提交動態排序參數的格式
<code>@RequestParam(value = "columns", defaultValue = "createDate") String[] columns,
@RequestParam(value = "orders", defaultValue = "desc") String[] orders
/<code>
例如:id升序,name逆序,那麼檢索參數就是: /foo?columns=id,name&orders=asc,desc多個參數,和多個排序策略使用逗號分隔。一一對應。
PageUtils
封裝一個方法,通過排序字段和排序策略,生成排序SQL語句。在方法中完成對字段,排序策略合法性的檢查。
<code>public class PageUtils {
\t
\tstatic final String DEFAULT_ORDER = "ASC";
\t
\tpublic static String order(String[] columns, String[] orders) {
\t\tif (columns == null || columns.length == 0) {
\t\t\treturn "";
\t\t}
\t\tStringBuilder stringBuilder = new StringBuilder();
\t\tfor (int x = 0; x < columns.length; x++) {
\t\t\tString column = columns[x];
\t\t\tString order = null;
\t\t\t
\t\t\tif (orders != null && orders.length > x) {
\t\t\t\torder = orders[x].toUpperCase();
\t\t\t\tif (!(order.equals("ASC") || order.equals("DESC"))) {
\t\t\t\t\tthrow new IllegalArgumentException("非法的排序策略:" + column);
\t\t\t\t}
\t\t\t}else {
\t\t\t\torder = DEFAULT_ORDER;
\t\t\t}
\t\t\t// 判斷列名稱的合法性,防止SQL注入。只能是【字母,數字,下劃線】
\t\t\tif (!column.matches("[A-Za-z0-9_]+")) {
\t\t\t\tthrow new IllegalArgumentException("非法的排序字段名稱:" + column);
\t\t\t}
\t\t\t// 駝峰轉換為下劃線
\t\t\tcolumn = humpConversionUnderscore(column);
\t\t\t
\t\t\tif (x != 0) {
\t\t\t\tstringBuilder.append(", ");
\t\t\t}
\t\t\tstringBuilder.append("`" + column + "` " + order);
\t\t}
\t\treturn stringBuilder.toString();
\t}
\tpublic static String humpConversionUnderscore(String value) {
\t\tStringBuilder stringBuilder = new StringBuilder();
\t\tchar[] chars = value.toCharArray();
\t\tfor (char charactor : chars) {
\t\t\tif (Character.isUpperCase(charactor)) {
\t\t\t\tstringBuilder.append("_");
\t\t\t\tcharactor = Character.toLowerCase(charactor);
\t\t\t}
\t\t\tstringBuilder.append(charactor);
\t\t}
\t\treturn stringBuilder.toString();
\t}
}
/<code>
演示
<code>String orderSql = PageUtils.order(new String[] {"id", "name"}, new String[] {"ASC", "DESC"});
PageHelper.offsetPage(1, 10).setOrderBy(orderSql);
List<fooentity> foos = this.fooMapper.foos();
foos.forEach(System.out::println);
// SQL語句:String orderSql = PageUtils.order(new String[] {"id", "name"}, new String[] {"ASC", "DESC"});
PageHelper.offsetPage(1, 10).setOrderBy(orderSql);
List<fooentity> foos = this.fooMapper.foos();
foos.forEach(System.out::println);
/<fooentity>/<fooentity>/<code>
客戶端動態排序顯得非常靈活,並且沒有SQL注入的風險。最多也就是客戶端惡意提交排序的字段,在檢索結果集中找不到,導致SQL異常。但是不會導致執行危險的SQL語句。
原文:https://springboot.io/t/topic/905
閱讀更多 泡泡糖就是糖 的文章