解決mybatis-plus動態排序,導致的SQL注入問題

解決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


分享到:


相關文章: