一、背景
上周五有個朋友說,防sql注入都用參數化的方法,但是有些地方是不能參數化的。比如order by后就不能參數化,她有個同事挖sql注入時找有排序功能需求的位置(比如博客常按時間排序),基本十之六七都能挖到sql注入。
某些地方不能參數化,這個問題在以前面試時有被問過,但回答不上來,后來也沒太關心,所以確實也就一直不太懂。
order by后不能參數化我是第一次聽說的,進一步追問原理她歸究為mysql的bug,這說法實在不敢恭維,所以就找時間自己研究了一下。
二、不能參數化的根本原因
2.1 以java為例進行說明
典型的java寫的sql執行代碼片段如下:
Connection conn = DBConnect.getConnection(); PreparedStatement ps = null; ResultSet rs=null; String sql = " SELECT passwd FROM test_table1 WHERE username = ? "; ps = conn.prepareStatement(sql); # 通過setString()指明該參數是字符串類型 ps.setString(1, username); # 另外還有setInt()等一些其他方法 # ps.setInt(2, test_param); rs = ps.executeQuery();
ps.setString(1, username)會自動給值加上引號。比如假設username=“ls”,那么拼湊成的語句會是String sql = " SELECT passwd FROM test_table1 WHERE username = 'ls' ";
再看order by,order by后一般是接字段名,而字段名是不能帶引號的,比如 order by username;如果帶上引號成了order by 'username',那username就是一個字符串不是字段名了,這就產生了語法錯誤。
所以order by后不能參數化的本質是:一方面預編譯又只有自動加引號的setString()方法,沒有不加引號的方法;而另一方面order by后接的字段名不能有引號。(至於為什么不弄個能不自動加引號的set方法那就不太懂了)
更本質的說法是:不只order by,凡是字符串但又不能加引號的位置都不能參數化;包括sql關鍵字、庫名表名字段名函數名等等。
2.2 不能參數化位置的防sql注入辦法
不能參數化的位置,不管怎么拼接,最終都是和使用“+”號拼接字符串的功效一樣:拼成了sql語句但沒有防sql注入的效果。
但好在的一點是,不管是sql關鍵字,還是庫名表名字段名函數名對於后台開發者來說他的集合都是有限的,更准確點應該說也就那么幾個。
這時我們應可以使用白名單的這種針對有限集合最常用的處理辦法進行處理,如果傳來的參數不在白名單列表中,直接返回錯誤即可。
代碼類似如下:
if para_str.equals("key_str1"){ ...; } else if test_str.equals("key_str2"){ ...; } else{ throw new Exception("parameter error."); }
三、python中如何使用預編譯
import pymysql class TestDB(): def __init__(self): # 數據庫連接信息,改成自己的。ip-用戶名-密碼-數據庫名 self.test_db = pymysql.connect("192.168.220.128","root","toor","test_db") # 游標 self.cursor = self.test_db.cursor() def query_password_by_username(self, username): # 我們知道python中構造字符串的方法一般有四種 # 第一種。+號拼接形式,形如: # self.cursor.execute("SELECT passwd FROM test_table1 WHERE username = '" + username + "'") # 第二種。format()形式,形如: # self.cursor.execute("SELECT passwd FROM test_table1 WHERE username = '{}'".format(username)) # 第三種。最新的f-string形式,形如: # self.cursor.execute(f"SELECT passwd FROM test_table1 WHERE username = '{username}'") # 第四種。現在仍比較多見的%s形式,形如: # self.cursor.execute("SELECT passwd FROM test_table1 WHERE username = '%s'" % (username)) # 但不管是以上四種的哪一種(包括可能這里沒提到的一些字符串構造寫法),他們都沒有預防sql注入的效果 # 從他們自身角度說,他們本來就是為了構造字符串,並不是專門為了構造sql語句同時防止sql注入 # 從pymysql角度說,他接收到的就是一個寫好的sql語句,沒有任何特征可供他判斷這條sql語句有沒有被注入過,他只能直接執行 # pymysql本身有防sql注入的功能,啟用該功能的寫法如下: self.cursor.execute("SELECT passwd FROM test_table1 WHERE username = %s", (username)) # 在寫法上,相較於第四種%s構造形式差別不大,一是用逗號(,)代替了百分號(%);二是%s兩邊去掉了單引號。但有着本質上的差別 # %先格式化成一條語句交給pymysql執行,pymysql並不能參與sql語句的構造,只能傳過來什么執行什么 # ,相當於分成模板和參數元組兩個參數傳給pymysql,最后的sql語句由pymysql構造而成,這樣pymysql就能處理參數防止sql注入 # 不過要注意,如果參數本身是整形而不是字符串類型,那么這種方法雖然也會有異常,但其實是有正確結果輸出的。 # 主要的問題在於我不知道execute()本質上是怎么實現防sql注入的。 user_record = self.cursor.fetchone() return user_record if __name__ == "__main__": obj = TestDB() # 正常形式 username = "ls" print(f"normal given username is: {username}") user_record = obj.query_password_by_username(username) print(f"the password is: {user_record[0]}\n") # 注入形式 username = "ls' and 1 = 2 union select version() -- " print(f"inject given username is: {username}") user_record = obj.query_password_by_username(username) print(f"the password is: {user_record[0]}\n")
使用預編譯結果如下,注入失敗:
不使用預編譯,使用一般字符串構造 結果如下,注入成功:
參考: