SQL注入如何預防?
本文參考自owasp,重點是提供清晰,簡單,可操作的指導,以防止應用程序中的SQL注入漏洞。不幸的是,SQL注入攻擊很常見,這是由於兩個因素:
- SQL注入漏洞的顯着流行
- 目標的吸引力(即數據庫通常包含應用程序的所有有趣/關鍵數據)。
發生了如此多的成功SQL注入攻擊有點可恥,因為在代碼中避免SQL注入漏洞非常簡單。
當軟件開發人員創建包含用戶提供的輸入的動態數據庫查詢時,會引入SQL注入漏洞。為了避免SQL注入缺陷很簡單。開發人員需要:
a)停止編寫動態查詢;
b)防止用戶提供的包含惡意SQL的輸入影響所執行查詢的邏輯。
本文提供了一組通過避免這兩個問題來防止SQL注入漏洞的簡單技術。這些技術幾乎可以與任何類型的數據庫一起使用。還有其他類型的數據庫,如XML數據庫,可能有類似的問題(例如,XPath和XQuery注入),這些技術也可用於保護它們。
主要防御:
- 選項1:使用准備好的語句(帶參數化查詢)
- 選項2:使用存儲過程
- 選項3:白名單輸入驗證
- 選項4:轉義所有用戶提供的輸入
額外防御:
- 另外:強制執行最低權限
- 另外:執行白名單輸入驗證作為輔助防御
不安全的例子:
SQL注入漏洞通常如下所示:
以下(Java)示例是UNSAFE,並允許攻擊者將代碼注入將由數據庫執行的查詢中。簡單地附加到查詢的未經驗證的“customerName”參數允許攻擊者注入他們想要的任何SQL代碼。不幸的是,這種訪問數據庫的方法太常見了。
String query = "SELECT account_balance FROM user_data WHERE user_name = " + request.getParameter("customerName"); try { Statement statement = connection.createStatement( ... ); ResultSet results = statement.executeQuery( query ); } ...
主要防御
防御選項1:准備好的語句(帶參數化查詢)
使用帶有變量綁定的預准備語句(也就是參數化查詢)是所有開發人員應該首先學習如何編寫數據庫查詢的方法。它們比動態查詢更易於編寫,更易於理解。參數化查詢強制開發人員首先定義所有SQL代碼,然后將每個參數傳遞給查詢。這種編碼風格允許數據庫區分代碼和數據,無論提供什么用戶輸入。
准備好的語句可確保攻擊者無法更改查詢的意圖,即使攻擊者插入了SQL命令也是如此。在下面的安全示例中,如果攻擊者輸入的是userID tom' or '1'='1
,則參數化查詢不會受到攻擊,而是會查找與字符串完全匹配的用戶名tom' or '1'='1
。
特定語言的建議:
- Java EE -
PreparedStatement()
與綁定變量一起使用 - .NET - 使用參數化查詢,如綁定變量
SqlCommand()
或OleDbCommand()
使用綁定變量 - PHP - 將PDO與強類型參數化查詢一起使用(使用bindParam())
- Hibernate -
createQuery()
與綁定變量一起使用(在Hibernate中稱為命名參數) - SQLite - 用於
sqlite3_prepare()
創建語句對象
在極少數情況下,准備好的陳述會損害績效。遇到這種情況時,最好是a)強烈驗證所有數據或b)使用特定於數據庫供應商的轉義例程來轉義所有用戶提供的輸入,如下所述,而不是使用預准備語句。
安全JavaSQL語句示例
以下代碼示例使用PreparedStatement
Java的參數化查詢實現來執行相同的數據庫查詢。
// 一定要驗證 String custname = request.getParameter("customerName"); String query = "SELECT account_balance FROM user_data WHERE user_name = ? "; PreparedStatement pstmt = connection.prepareStatement( query ); pstmt.setString( 1, custname); ResultSet results = pstmt.executeQuery( );
安全C#.NET SQL語句示例
使用.NET,它更加直接。查詢的創建和執行不會更改。您所要做的就是使用Parameters.Add()
此處所示的調用將參數傳遞給查詢。
String query = "SELECT account_balance FROM user_data WHERE user_name = ?"; try { OleDbCommand command = new OleDbCommand(query, connection); command.Parameters.Add(new OleDbParameter("customerName", CustomerName Name.Text)); OleDbDataReader reader = command.ExecuteReader(); // … } catch (OleDbException se) { // error handling }
我們已經在Java和.NET中展示了示例,但實際上所有其他語言(包括Cold Fusion和Classic ASP)都支持參數化查詢接口。甚至SQL抽象層,如Hibernate查詢語言(HQL)也有相同類型的注入問題(我們稱之為HQL注入)。HQL也支持參數化查詢,因此我們可以避免這個問題:
Hibernate查詢語言(HQL)准備語句(命名參數)示例
//First is an unsafe HQL Statement Query unsafeHQLQuery = session.createQuery("from Inventory where productID='"+userSuppliedParameter+"'"); //Here is a safe version of the same query using named parameters Query safeHQLQuery = session.createQuery("from Inventory where productID=:productid"); safeHQLQuery.setParameter("productid", userSuppliedParameter);
開發人員傾向於喜歡Prepared Statement方法,因為所有SQL代碼都保留在應用程序中。這使您的應用程序相對數據庫獨立。
防御選項2:存儲過程
SQL注入並不總是安全的存儲過程。但是,某些標准存儲過程編程結構與安全實現時使用參數化查詢具有相同的效果,這是大多數存儲過程語言的標准。
它們要求開發人員只使用自動參數化的參數構建SQL語句,除非開發人員在很大程度上超出了標准。預准備語句和存儲過程之間的區別在於,存儲過程的SQL代碼已定義並存儲在數據庫本身中,然后從應用程序中調用。這兩種技術在防止SQL注入方面具有相同的效果,因此您的組織應該選擇哪種方法對您最有意義。
注意:'安全實現'意味着存儲過程不包含任何不安全的動態SQL生成。開發人員通常不會在存儲過程中生成動態SQL。但是,它可以做到,但應該避免。如果無法避免,則存儲過程必須使用輸入驗證或本文所述的正確轉義,以確保不能使用所有用戶提供的存儲過程輸入將SQL代碼注入動態生成的查詢中。審計人員應始終在SQL Server存儲過程中查找sp_execute,execute或exec的用法。類似的審計指南對於其他供應商的類似功能是必要的。
在某些情況下,存儲過程可能會增加風險。例如,MS SQL服務器上,你有3個主要的默認角色:db_datareader
,db_datawriter
和db_owner
。在存儲過程開始使用之前,DBA會根據要求為webservice的用戶提供db_datareader或db_datawriter權限。但是,存儲過程需要執行權限,默認情況下該角色不可用。用戶管理已集中在一些設置,但僅限於這3個角色,導致所有Web應用程序在db_owner權限下運行,因此存儲過程可以正常工作。當然,這意味着如果服務器遭到破壞,攻擊者擁有數據庫的完全權限,以前他們可能只具有讀訪問權限。
安全Java存儲過程示例
以下代碼示例使用CallableStatement
Java的存儲過程接口實現來執行相同的數據庫查詢。該sp_getAccountBalance
存儲過程將在數據庫中被預先定義和執行相同的功能與上述定義的查詢。
// This should REALLY be validated String custname = request.getParameter("customerName"); try { CallableStatement cs = connection.prepareCall("{call sp_getAccountBalance(?)}"); cs.setString(1, custname); ResultSet results = cs.executeQuery(); // … result set handling } catch (SQLException se) { // … logging and error handling }
安全的VB .NET存儲過程示例
以下代碼示例使用SqlCommand
.NET的存儲過程接口實現來執行相同的數據庫查詢。該sp_getAccountBalance
存儲過程將在數據庫中被預先定義和執行相同的功能與上述定義的查詢。
Try Dim command As SqlCommand = new SqlCommand("sp_getAccountBalance", connection) command.CommandType = CommandType.StoredProcedure command.Parameters.Add(new SqlParameter("@CustomerName", CustomerName.Text)) Dim reader As SqlDataReader = command.ExecuteReader() '... Catch se As SqlException 'error handling End Try
防御選項3:白名單輸入驗證
SQL查詢的各個部分不是使用綁定變量的合法位置,例如表或列的名稱,以及排序順序指示符(ASC或DESC)。在這種情況下,輸入驗證或查詢重新設計是最合適的防御。對於表或列的名稱,理想情況下,這些值來自代碼,而不是來自用戶參數。
但是,如果用戶參數值用於使表名和列名不同,則應將參數值映射到合法/預期的表或列名,以確保未經驗證的用戶輸入不會在查詢中結束。請注意,這是設計不佳的症狀,如果時間允許,應考慮完全重寫。
以下是表名驗證的示例。
String tableName; switch(PARAM): case "Value1": tableName = "fooTable"; break; case "Value2": tableName = "barTable"; break; ... default : throw new InputValidationException("unexpected value provided" + " for table name");
該tableName
然后可以直接附加到SQL查詢,因為它是目前已知的是在此查詢表名的法律和預期值之一。請記住,通用表驗證功能可能會導致數據丟失,因為表名用於不期望它們的查詢中。
對於像排序順序這樣簡單的東西,最好將用戶提供的輸入轉換為布爾值,然后使用該布爾值選擇要附加到查詢的安全值。這是動態查詢創建中非常標准的需求。
例如:
public String someMethod(boolean sortOrder) { String SQLquery = "some SQL ... order by Salary " + (sortOrder ? "ASC" : "DESC");` ...
任何時候用戶輸入都可以轉換為非String,如日期,數字,布爾值,枚舉類型等,然后將其附加到查詢中,或用於選擇要追加到查詢的值,這可以確保它是這樣做是安全的。
在所有情況下,也建議將輸入驗證作為輔助防御,即使使用綁定變量,如本文后面所述。有關如何實施強白名單輸入驗證的更多技術在輸入驗證備忘單中進行了描述。
防御選項4:轉義所有用戶提供的輸入
當上述任何一種方法都不可行時,該技術僅應作為最后的手段使用。輸入驗證可能是一個更好的選擇,因為與其他防御相比,這種方法很脆弱,我們不能保證它會在所有情況下阻止所有SQL注入。
此技術是在將用戶輸入放入查詢之前將其轉義。它的實現在數據庫方面非常具體。通常只建議在實現輸入驗證時不會降低遺留代碼的成本效益。應該使用參數化查詢,存儲過程或某種為您構建查詢的對象關系映射器(ORM)來構建或重寫從頭開始構建的應用程序或需要低風險容忍度的應用程序。
這種技術就是這樣的。每個DBMS都支持一種或多種特定於某些查詢的字符轉義方案。如果您使用正在使用的數據庫的正確轉義方案轉義所有用戶提供的輸入,則DBMS不會將該輸入與開發人員編寫的SQL代碼混淆,從而避免任何可能的SQL注入漏洞。
要專門為數據庫編碼器查找javadoc,請單擊Codec
左側的類。有很多編解碼器實現。兩個特定於數據庫的編解碼器是OracleCodec
,和MySQLCodec
。
只需All Known Implementing Classes:
在Interface Codec頁面頂部單擊其名稱即可。
目前,ESAPI目前擁有以下數據庫編碼器:
- MySQL(支持ANSI和本機模式)
數據庫編碼器即將推出:
- SQL Server
- PostgreSQL的
如果您的數據庫編碼器丟失,請告訴我們。
特定於數據庫的轉義詳細信息
如果您想構建自己的轉義例程,以下是我們為ESAPI編碼器開發的每個數據庫的轉義細節:
- SQL Server
- DB2
轉義動態查詢
使用ESAPI數據庫編解碼器非常簡單。Oracle示例如下所示:
ESAPI.encoder().encodeForSQL( new OracleCodec(), queryparam );
因此,如果您在代碼中生成了一個現有的動態查詢,該查詢將轉到Oracle,如下所示:
String query = "SELECT user_id FROM user_data WHERE user_name = '" + req.getParameter("userID") + "' and user_password = '" + req.getParameter("pwd") +"'"; try { Statement statement = connection.createStatement( … ); ResultSet results = statement.executeQuery( query ); }
你會重寫第一行看起來像這樣:
Codec ORACLE_CODEC = new OracleCodec(); String query = "SELECT user_id FROM user_data WHERE user_name = '" + ESAPI.encoder().encodeForSQL( ORACLE_CODEC, req.getParameter("userID")) + "' and user_password = '" + ESAPI.encoder().encodeForSQL( ORACLE_CODEC, req.getParameter("pwd")) +"'";
無論輸入是什么,它現在都可以安全地進行SQL注入。
為了獲得最大的代碼可讀性,您還可以構建自己的代碼OracleEncoder
:
Encoder oe = new OracleEncoder(); String query = "SELECT user_id FROM user_data WHERE user_name = '" + oe.encode( req.getParameter("userID")) + "' and user_password = '" + oe.encode( req.getParameter("pwd")) +"'";
使用這種類型的解決方案,您只需要將每個用戶提供的參數包裝成一個ESAPI.encoder().encodeForOracle( )
調用或者您命名為調用的任何內容,您就可以完成。
在Like子句中轉義通配符
該LIKE
關鍵字允許進行文本掃描搜索。在Oracle中,下划線_
字符僅匹配一個字符,而&符號%
用於匹配任何字符的零次或多次出現。必須在LIKE子句條件中轉義這些字符。
例如:
SELECT name FROM emp WHERE id LIKE '%/_%' ESCAPE '/';
SELECT name FROM emp WHERE id LIKE '%\%%' ESCAPE '\';
MySQL轉義
MySQL支持兩種轉義模式:
ANSI_QUOTES
SQL模式,以及這個關閉的模式,我們稱之為MySQL
模式。
ANSI SQL
模式:'
使用''
(兩個單一刻度)簡單編碼所有(單個刻度)字符
MySQL
模式,執行以下操作:
NUL (0x00) --> \0 [This is a zero, not the letter O] BS (0x08) --> \b TAB (0x09) --> \t LF (0x0a) --> \n CR (0x0d) --> \r SUB (0x1a) --> \Z " (0x22) --> \" % (0x25) --> \% ' (0x27) --> \' \ (0x5c) --> \\ _ (0x5f) --> \_ all other non-alphanumeric characters with ASCII values less than 256 --> \c where 'c' is the original non-alphanumeric character.
SQL Server轉義
我們還沒有實現SQL Server轉義例程,但是下面有很好的指針和鏈接到描述如何防止SQL服務器上的SQL注入攻擊的文章,請參見此處。
DB2轉義
此信息基於DB2 WebQuery特殊字符以及Oracle JDBC DB2驅動程序中的一些信息。
有關幾個DB2 Universal驅動程序之間差異的信息。
十六進制編碼所有輸入
轉義的一個特殊情況是對從用戶接收的整個字符串進行十六進制編碼的過程(這可以看作是轉義每個字符)。Web應用程序應在將用戶輸入包含在SQL語句中之前對其進行十六進制編碼。SQL語句應該考慮到這一事實,並相應地比較數據。
例如,如果我們必須查找匹配sessionID的記錄,並且用戶將字符串abc123作為會話ID發送,則select語句將為:
SELECT ... FROM session WHERE hex_encode(sessionID) = '616263313233'
hex_encode
應該由所使用的數據庫的特定工具替換。字符串606162313233是從用戶接收的字符串的十六進制編碼版本(它是用戶數據的ASCII / UTF-8代碼的十六進制值的序列)。
如果攻擊者要傳輸包含單引號字符的字符串,然后嘗試注入SQL代碼,則構造的SQL語句將只顯示如下:
... WHERE hex_encode ( ... ) = '2720 ... '
27
是單引號的ASCII代碼(十六進制),它與字符串中的任何其他字符一樣只是十六進制編碼。產生的SQL只能包含數字數字和字母a
來f
,從來沒有任何特殊字符,它可能會使SQL注入。
在PHP中轉義SQLi
使用預准備語句和參數化查詢。這些是由數據庫服務器與任何參數分開發送和解析的SQL語句。這樣攻擊者就無法注入惡意SQL。
你基本上有兩個選擇來實現這個目標:
- 使用PDO(適用於任何支持的數據庫驅動程序):
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name'); $stmt->execute(array('name' => $name)); foreach ($stmt as $row) { // do something with $row }
- 使用MySQLi(用於MySQL):
$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?'); $stmt->bind_param('s', $name); $stmt->execute(); $result = $stmt->get_result(); while ($row = $result->fetch_assoc()) { // do something with $row }
PDO是通用選項。如果您要連接到MySQL以外的數據庫,則可以引用特定於驅動程序的第二個選項(例如,對於PostgreSQL,請使用pg_prepare()和pg_execute())。
額外的防御
除了采用四種主要防御之一外,我們還建議采用所有這些額外的防御措施,以便提供深度防御。這些額外的防御是:
- 最低權限
- 白名單輸入驗證
最低權限
為了最大限度地減少成功SQL注入攻擊的潛在損害,您應該最小化分配給環境中每個數據庫帳戶的權限。不要為您的應用程序帳戶分配DBA或管理員類型訪問權限。我們知道這很容易,當你這樣做時,一切都“有效”,但這是非常危險的。
從頭開始確定您的應用程序帳戶需要哪些訪問權限,而不是試圖找出您需要帶走的訪問權限。確保僅需要讀訪問權限的帳戶才被授予對他們需要訪問的表的讀訪問權限。
如果帳戶只需要訪問表的某些部分,請考慮創建一個視圖,以限制對該部分數據的訪問,並為帳戶分配帳戶訪問權限,而不是基礎表。很少,如果有的話,授予對數據庫帳戶的創建或刪除訪問權限。
如果您采用的策略是在任何地方使用存儲過程,並且不允許應用程序帳戶直接執行自己的查詢,那么將這些帳戶限制為只能執行所需的存儲過程。不要直接向數據庫中的表授予任何權限。
SQL注入不是對數據庫數據的唯一威脅。攻擊者可以簡單地將參數值從它們所呈現的合法值之一更改為未經授權的值,但應用程序本身可能被授權訪問。因此,盡量減少授予應用程序的權限將降低此類未經授權的訪問嘗試的可能性,即使攻擊者沒有嘗試將SQL注入用作其漏洞利用的一部分。
在您使用它時,您應該最小化DBMS運行的操作系統帳戶的權限。不要以root用戶身份或系統運行DBMS!大多數DBMS都是開箱即用的,具有非常強大的系統帳戶。例如,默認情況下,MySQL在Windows上作為系統運行!使用受限制的權限將DBMS的OS帳戶更改為更合適的帳戶。
多個DB用戶
Web應用程序的設計者不僅應避免在Web應用程序中使用相同的所有者/管理員帳戶來連接到數據庫。不同的DB用戶可以用於不同的Web應用程序。
通常,需要訪問數據庫的每個單獨的Web應用程序都可以具有指定的數據庫用戶帳戶,Web應用程序將使用該帳戶連接到數據庫。這樣,應用程序的設計者可以在訪問控制中具有良好的粒度,從而盡可能地減少特權。然后,每個數據庫用戶都可以選擇訪問它所需的內容,並根據需要進行寫訪問。
例如,登錄頁面需要對表的用戶名和密碼字段進行讀訪問,但不能對任何表單進行寫訪問(無插入,更新或刪除)。但是,注冊頁面當然需要對該表的插入權限; 只有當這些Web應用程序使用不同的DB用戶連接到數據庫時,才能強制執行此限制。
查看
通過限制對表的特定字段或表的連接的讀訪問,可以使用SQL視圖進一步增加訪問的粒度。它可能具有額外的好處:例如,假設系統需要(可能由於某些特定的法律要求)來存儲用戶的密碼,而不是鹽漬的密碼。
設計師可以使用視圖來彌補這種限制; 撤消對表的所有訪問(來自除所有者/管理員之外的所有數據庫用戶)並創建一個輸出密碼字段的哈希而不是字段本身的視圖。任何成功竊取數據庫信息的SQL注入攻擊都將被限制為竊取密碼的哈希值(甚至可能是鍵控哈希值),因為任何Web應用程序的數據庫用戶都無權訪問表本身。