SQL注入介紹及預防
SQL注入介紹
在Web程式中,一般都會有後臺根據使用者輸入內容查詢或者執行相關動作的場景,如登入時查詢使用者是否存在。後臺在處理的時候可能是根據使用者輸入的使用者名稱,拼接SQL之後到資料庫查詢來判斷,這時,如果使用者惡意輸入不正確內容或內容本身存在問題,會導致應用程式崩潰,甚至是丟失資料等導致相關損失。即通過把SQL命令插入到Web表單提交或輸入域名或頁面請求的查詢字串,最終達到欺騙伺服器執行惡意的SQL命令。這種場景就稱為SQL注入。
一個SQL注入的簡單例子
如執行一條SQL語句:
select * from tb_user where username = 'jacobzhou';
其中jacobzhou是使用者輸入的值,此時可以得到正確的值:
mysql> select * -> from tb_user -> where username = 'jacobzhou'; +------+-----------+----------+------+ | id | username | password | age | +------+-----------+----------+------+ | 1 | jacobzhou | 123456 | 29 | +------+-----------+----------+------+ 1 row in set (0.00 sec)
但是,當用戶輸入錯誤的值,如jacobzhou';drop table tb_test;,如果使用的是字串拼接的方式去執行,SQL語句就變成了:
select *
from tb_user
where username = 'jacobzhou';drop table tb_test;';
執行結果為:
mysql> select * -> from tb_user -> where username = 'jacobzhou';drop table tb_test;'; +------+-----------+----------+------+ | id | username | password | age | +------+-----------+----------+------+ | 1 | jacobzhou | 123456 | 29 | +------+-----------+----------+------+ 1 row in set (0.00 sec) ERROR 1051 (42S02): Unknown table 'db-platform.tb_test' '>
以上就是一個SQL注入的例子,可以看到,如果db-platform.tb_test表存在,那就會被惡意刪除。例子相對較極端,對於Django自帶的connection來說,使用execute函式執行SQL語句的時候,每次只執行一條語句,後一條語句不會執行。因此上面的例子在Django中是不成立的,但是足以說明SQL注入所帶來的安全風險。那SQL注入都有哪些方式,如何才能防止SQL注入呢?
SQL注入原理
Web應用程式對於使用者輸入的資料和合法性沒有嚴謹的判斷,前端使用者的輸入直接傳輸給後端,攻擊者通過構造不同的引數,形成不同的SQL語句來實現對資料庫的任意操作。
SQL注入產生需要滿足兩個條件:
- 引數使用者可控:前端傳給後端的引數內容是使用者可以控制的
- 引數帶入資料庫查詢:傳入的引數直接拼接到SQL語句,且帶入資料庫查詢
SQL注入型別
SQL注入的分類有很多,如POST注入、Cookie注入、延時注入、搜尋注入等,但是歸根結底也是數字型和字元型注入的不同展現形式或者是注入的位置不同。
數字型
使用者輸入為整數,假設SQL語句為:
select * from home_application_database where id = 3;
其中3為數字,是使用者正常輸入。
當滿足如下條件,則可能存在數字型注入:
- 輸入3' 頁面報錯(SQL語法錯誤)
- 輸入3 and 1 = 1 頁面正常返回結果
- 輸入3 and 1=2 頁面返回錯誤(SQL語句返回空資料)
如果後臺使用的是select * from home_application_database where id =
和未經驗證的使用者輸入做拼接後去資料庫查詢,就滿足了上面的三個條件,存在數字型注入。
這裡可以看到使用者輸入必須是整數,後端驗證使用者輸入必須是整數才會繼續執行即可解決。
字元型
使用者輸入是字串,如SQL語句:
select * from home_application_database where name = '154'
其中154是字串,是使用者正常輸入。
當滿足如下條件時,則可能存在字元型注入:
- 輸入154',頁面異常(SQL語句語法錯誤)
- 輸入154' and 1 = 1 -- ,頁面正常返回
- 輸入154' and 1 = 2 -- ,頁面錯誤(查詢結果為空)
同數字型,如果後臺直接使用拼接語句的形式去資料庫執行,則就滿足了上面單個條件,存在字元型注入。攻擊者使用單引號的方式提前結束前一個單引號,並使用and來新增其他操作。
如何防止SQL注入
在開發時應該秉持一種外部引數皆不可信的原則來進行開發。
-
加強引數驗證
開發時,驗證所有來自前端的輸入,必須是符合要求的資料型別,符合指定規則的資料才允許繼續往下執行。
-
SQL語句引數化處理
減少使用或不使用字串拼接的方式執行SQL,而是將使用者輸入當著引數傳給執行SQL的方法,如Django中的cursor.execute()函式就支援在SQL語句中使用佔位符,將輸入作為引數傳遞給方法執行。
-
儲存過程
使用儲存過程也可以有效防止SQL注入,不過在儲存過程中,需使用佔位符,並且使用輸入引數來預編譯SQL語句後再執行。
Django中防止SQL注入
Django中使用ORM可以有效防止SQL注入,所以應該儘可能使用ORM。但是ORM對於複雜查詢就無能為力了,這時就需要執行原生SQL時,可以使用如下方式:
- 使用extra(不建議使用這種方式執行SQL)
- 使用raw
- 使用django.db執行自定義SQL
- 直接使用pymysql
在使用原生SQL語句時,應避免直接使用使用者輸入拼接SQL語句,上面三種執行原生SQL的方式均提供了佔位符來進行引數替換,防止SQL注入。我們比較常用的是3、和4,兩種方法都是使用cursor.execue()方法,具體如下:
-
django.db
from django.db import connection cursor = connection.cursor() cursor.execute(sql) result = cursor.fetchall()
-
pymysql
connection = pymysql.connect(**mysql_server) cursor = connection.cursor() cursor.execute(sql) result = cursor.fetchall()
execute中的sql語句使用佔位符,並傳入相應引數即可防止SQL注入:
sql="select * from home_application_database where id = %s" cursor.execute(sql,3) 1 cursor.execute(sql,"3'") 1 E:\venv\python2\lib\site-packages\pymysql\cursors.py:297: Warning: Truncated incorrect DOUBLE value: '3'' self._do_get_result() cursor.execute(sql,"3 and 1 = 1 ") E:\venv\python2\lib\site-packages\pymysql\cursors.py:297: Warning: Truncated incorrect DOUBLE value: '3 and 1 = 1 ' 1 self._do_get_result() cursor.execute(sql,"3 and 1 = 2 ") E:\venv\python2\lib\site-packages\pymysql\cursors.py:297: Warning: Truncated incorrect DOUBLE value: '3 and 1 = 2 ' self._do_get_result() 1
從上面執行結果看出,對於整數型注入,使用execute中帶引數執行的方式,並不滿足注入條件,當使用3',3 and 1 = 1 ,3 and 1 = 2 作為輸入傳給execute執行時,程式報錯。
同理對於字元型注入也一樣。
注意:在使用execute函式執行時,SQL語句中的佔位符,不管是字元還是整型,都使用%s,且對於字元型的資料,在SQL語句裡面不能使用'%s',否則會報錯。使用引數替換本質上是對輸入的引數進行轉義處理,防止輸入中的引號。