SQL 注入詳解

一:什麼是 sql 注入

SQL 注入是比較常見的網絡攻擊方式之一,它不是利用操作系統的 BUG 來實現攻擊,而是針對程序員編寫時的疏忽,通過 SQL 語句,實現無賬號登錄,甚至篡改數據庫。

二:SQL 注入攻擊的總體思路

1:尋找到 SQL 注入的位置

2:判斷服務器類型和後臺數據庫類型

3:針對不同的服務器和數據庫特點進行 SQL 注入攻擊

三:SQL 注入攻擊實例

String sql = "select * from user_table where username=
' "+userName+" ' and password=' "+password+" '";

--當輸入了上面的用戶名和密碼,上面的SQL語句變成:
SELECT * FROM user_table WHERE username=
'’or 1 = 1 -- and password='"""
--分析SQL語句:
--條件後面username=”or 1=1 用戶名等於 ” 或1=1 那麼這個條件一定會成功;

--然後後面加兩個-,這意味着註釋,它將後面的語句註釋,讓他們不起作用,這樣語句永遠都--能正確執行,用戶輕易騙過系統,獲取合法身份。
--這還是比較溫柔的,如果是執行
SELECT * FROM user_table WHERE
username='' ;DROP DATABASE (DB Name) --' and password=''
--其後果可想而知…
"""

四:如何防禦 SQL 注入

注意:但凡有 SQL 注入漏洞的程序,都是因爲程序要接受來自客戶端用戶輸入的變量或 URL 傳遞的參數,並且這個變量或參數是組成 SQL 語句的一部分,對於用戶輸入的內容或傳遞的參數,我們應該要時刻保持警惕,這是安全領域裏的「外部數據不可信任」的原則,縱觀 Web 安全領域的各種攻擊方式,大多數都是因爲開發者違反了這個原則而導致的,所以自然能想到的,就是從變量的檢測、過濾、驗證下手,確保變量是開發者所預想的。

1、檢查變量數據類型和格式

如果你的 SQL 語句是類似 where id={$id} 這種形式,數據庫裏所有的 id 都是數字,那麼就應該在 SQL 被執行前,檢查確保變量 id 是 int 類型;如果是接受郵箱,那就應該檢查並嚴格確保變量一定是郵箱的格式,其他的類型比如日期、時間等也是一個道理。總結起來:只要是有固定格式的變量,在 SQL 語句執行前,應該嚴格按照固定格式去檢查,確保變量是我們預想的格式,這樣很大程度上可以避免 SQL 注入攻擊。

比如,我們前面接受 username 參數例子中,我們的產品設計應該是在用戶註冊的一開始,就有一個用戶名的規則,比如 5-20 個字符,只能由大小寫字母、數字以及一些安全的符號組成,不包含特殊字符。此時我們應該有一個 check_username 的函數來進行統一的檢查。不過,仍然有很多例外情況並不能應用到這一準則,比如文章發佈系統,評論系統等必須要允許用戶提交任意字符串的場景,這就需要採用過濾等其他方案了。

2、過濾特殊符號

對於無法確定固定格式的變量,一定要進行特殊符號過濾或轉義處理。

3、綁定變量,使用預編譯語句

MySQL 的 mysqli 驅動提供了預編譯語句的支持,不同的程序語言,都分別有使用預編譯語句的方法

實際上,綁定變量使用預編譯語句是預防 SQL 注入的最佳方式,使用預編譯的 SQL 語句語義不會發生改變,在 SQL 語句中,變量用問號? 表示,黑客即使本事再大,也無法改變 SQL 語句的結構

五:什麼是 sql 預編譯

1.1:預編譯語句是什麼

通常我們的一條 sql 在 db 接收到最終執行完畢返回可以分爲下面三個過程:

我們把這種普通語句稱作 Immediate Statements。

但是很多情況,我們的一條 sql 語句可能會反覆執行,或者每次執行的時候只有個別的值不同(比如 query 的 where 子句值不同,update 的 set 子句值不同, insert 的 values 值不同)。  如果每次都需要經過上面的詞法語義解析、語句優化、制定執行計劃等,則效率就明顯不行了。

所謂預編譯語句就是將這類語句中的值用佔位符替代,可以視爲將 sql 語句模板化或者說參數化,一般稱這類語句叫 Prepared Statements 或者 Parameterized Statements

預編譯語句的優勢在於歸納爲:一次編譯、多次運行,省去了解析優化等過程;此外預編譯語句能防止 sql 注入。  當然就優化來說,很多時候最優的執行計劃不是光靠知道 sql 語句的模板就能決定了,往往就是需要通過具體值來預估出成本代價。

1.2:MySQL 的預編譯功能

注意 MySQL 的老版本(4.1 之前)是不支持服務端預編譯的,但基於目前業界生產環境普遍情況,基本可以認爲 MySQL 支持服務端預編譯。

下面我們來看一下 MySQL 中預編譯語句的使用。

(1)建表 首先我們有一張測試表 t,結構如下所示:
mysql> show create table t\G
*************************** 1. row ***************************
       Table: t
Create Table: CREATE TABLE `t` (
  `a` int(11) DEFAULT NULL,
  `b` varchar(20) DEFAULT NULL,
  UNIQUE KEY `ab` (`a`,`b`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
(2)編譯

我們接下來通過 PREPARE stmt_name FROM preparable_stm 的語法來預編譯一條 sql 語句

mysql> prepare ins from 'insert into t select ?,?';
Query OK, 0 rows affected (0.00 sec)
Statement prepared
(3)執行

我們通過 EXECUTE stmt_name [USING @var_name [, @var_name] ...] 的語法來執行預編譯語句

mysql> set @a=999,@b='hello';
Query OK, 0 rows affected (0.00 sec)
 
mysql> execute ins using @a,@b;
Query OK, 1 row affected (0.01 sec)
Records: 1  Duplicates: 0  Warnings: 0
 
mysql> select * from t;
+------+-------+
| a    | b     |
+------+-------+
|  999 | hello |
+------+-------+
1 row in set (0.00 sec)

可以看到,數據已經被成功插入表中。

MySQL 中的預編譯語句作用域是 session 級,但我們可以通過 max_prepared_stmt_count 變量來控制全局最大的存儲的預編譯語句。

mysql> set @@global.max_prepared_stmt_count=1;
Query OK, 0 rows affected (0.00 sec)
 
mysql> prepare sel from 'select * from t';
ERROR 1461 (42000): Can't create more than max_prepared_stmt_count statements (current value: 1)

當預編譯條數已經達到閾值時可以看到 MySQL 會報如上所示的錯誤。

(4)釋放

如果我們想要釋放一條預編譯語句,則可以使用 DEALLOCATEDROPPREPARE stmt_name 的語法進行操作:

mysql> deallocate prepare ins;
Query OK, 0 rows affected (0.00 sec)

六:爲什麼 PrepareStatement 可以防止 sql 注入

原理是採用了預編譯的方法,先將 SQL 語句中可被客戶端控制的參數集進行編譯,生成對應的臨時變量集,再使用對應的設置方法,爲臨時變量集裏面的元素進行賦值,賦值函數 setString(),會對傳入的參數進行強制類型檢查和安全檢查,所以就避免了 SQL 注入的產生。下面具體分析

搜索公衆號 Java 筆記蝦,回覆 “後端面試”,送你一份面試題大全. pdf

(1):爲什麼 Statement 會被 sql 注入

因爲 Statement 之所以會被 sql 注入是因爲 SQL 語句結構發生了變化。比如:

"select*from tablename where user+uesrname+  
"'and password='"+password+"'"

在用戶輸入'or true or'之後 sql 語句結構改變。

select*from tablename where username=''or true or'' and password=''

這樣本來是判斷用戶名和密碼都匹配時纔會計數,但是經過改變後變成了或的邏輯關係,不管用戶名和密碼是否匹配該式的返回值永遠爲 true;

(2)爲什麼 Preparement 可以防止 SQL 注入。

因爲 Preparement 樣式爲

select*from tablename where username=? and password=?

該 SQL 語句會在得到用戶的輸入之前先用數據庫進行預編譯,這樣的話不管用戶輸入什麼用戶名和密碼的判斷始終都是並的邏輯關係,防止了 SQL 注入

簡單總結,參數化能防注入的原因在於,語句是語句,參數是參數,參數的值並不是語句的一部分,數據庫只按語句的語義跑,至於跑的時候是帶一個普通揹包還是一個怪物,不會影響行進路線,無非跑的快點與慢點的區別。

七:mybatis 是如何防止 SQL 注入的

1、首先看一下下面兩個 sql 語句的區別:

<select id="selectByNameAndPassword" parameterType="java.util.Map" resultMap="BaseResultMap">
select id, username, password, role
from user
where username = #{username,jdbcType=VARCHAR}
and password = #{password,jdbcType=VARCHAR}
</select>

~

<select id="selectByNameAndPassword" parameterType="java.util.Map" resultMap="BaseResultMap">
select id, username, password, role
from user
where username = ${username,jdbcType=VARCHAR}
and password = ${password,jdbcType=VARCHAR}
</select>

mybatis 中的 #和 $ 的區別:

1、#將傳入的數據都當成一個字符串,會對自動傳入的數據加一個雙引號。
如:where username=#{username},如果傳入的值是 111, 那麼解析成 sql 時的值爲where user, 如果傳入的值是 id,則解析成的 sql 爲where user.

2、$將傳入的數據直接顯示生成在 sql 中。
如:where username=${username},如果傳入的值是 111, 那麼解析成 sql 時的值爲where username=111;如果傳入的值是; drop table user;,則解析成的 sql 爲:select id, username, password, role from user where username=;drop table user;

3、#方式能夠很大程度防止 sql 注入,$方式無法防止 Sql 注入。

4、$方式一般用於傳入數據庫對象,例如傳入表名.

5、一般能用#的就別用$,若不得不使用“${xxx}”這樣的參數,要手工地做好過濾工作,來防止 sql 注入攻擊。

6、在 MyBatis 中,“${xxx}”這樣格式的參數會直接參與 SQL 編譯,從而不能避免注入攻擊。但涉及到動態表名和列名時,只能使用“${xxx}”這樣的參數格式。所以,這樣的參數需要我們在代碼中手工進行處理來防止注入。

【結論】在編寫 MyBatis 的映射語句時,儘量採用“#{xxx}”這樣的格式。若不得不使用“${xxx}”這樣的參數,要手工地做好過濾工作,來防止 SQL 注入攻擊。**

mybatis 是如何做到防止 sql 注入的

MyBatis 框架作爲一款半自動化的持久層框架,其 SQL 語句都要我們自己手動編寫,這個時候當然需要防止 SQL 注入。其實,MyBatis 的 SQL 是一個具有 “輸入 + 輸出” 的功能,類似於函數的結構,參考上面的兩個例子。

搜索公衆號 Java 筆記蝦,回覆 “後端面試”,送你一份面試題大全. pdf

其中,parameterType 表示了輸入的參數類型,resultType 表示了輸出的參數類型。迴應上文,如果我們想防止 SQL 注入,理所當然地要在輸入參數上下功夫。上面代碼中使用 #的即輸入參數在 SQL 中拼接的部分,傳入參數後,打印出執行的 SQL 語句,會看到 SQL 是這樣的:

select id, username, password, role from user where username=? and password=?

不管輸入什麼參數,打印出的 SQL 都是這樣的。這是因爲 MyBatis 啓用了預編譯功能,在 SQL 執行前,會先將上面的 SQL 發送給數據庫進行編譯;執行時,直接使用編譯好的 SQL,替換佔位符 “?” 就可以了。因爲 SQL 注入只能對編譯過程起作用,所以這樣的方式就很好地避免了 SQL 注入的問題。

【底層實現原理】MyBatis 是如何做到 SQL 預編譯的呢?其實在框架底層,是 JDBC 中的 PreparedStatement 類在起作用,PreparedStatement 是我們很熟悉的 Statement 的子類,它的對象包含了編譯好的 SQL 語句。這種 “準備好” 的方式不僅能提高安全性,而且在多次執行同一個 SQL 時,能夠提高效率。原因是 SQL 已編譯好,再次執行時無需再編譯

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/orIksb3EQULYbvvWQyiyfA