預防SQL注入筆記

SQL注入如何預防?

本文參考自owasp,重點是提供清晰,簡單,可操做的指導,以防止應用程序中的SQL注入漏洞。不幸的是,SQL注入攻擊很常見,這是因爲兩個因素:php

  1. SQL注入漏洞的顯着流行
  2. 目標的吸引力(即數據庫一般包含應用程序的全部有趣/關鍵數據)。

發生瞭如此多的成功SQL注入攻擊有點可恥,由於在代碼中避免SQL注入漏洞很是簡單。html

當軟件開發人員建立包含用戶提供的輸入的動態數據庫查詢時,會引入SQL注入漏洞。爲了不SQL注入缺陷很簡單。開發人員須要:java

a)中止編寫動態查詢; mysql

b)防止用戶提供的包含惡意SQL的輸入影響所執行查詢的邏輯。git

 

本文提供了一組經過避免這兩個問題來防止SQL注入漏洞的簡單技術。這些技術幾乎能夠與任何類型的數據庫一塊兒使用。還有其餘類型的數據庫,如XML數據庫,可能有相似的問題(例如,XPath和XQuery注入),這些技術也可用於保護它們。github

主要防護:web

  • 選項1:使用準備好的語句(帶參數化查詢)
  • 選項2:使用存儲過程
  • 選項3:白名單輸入驗證
  • 選項4:轉義全部用戶提供的輸入

額外防護:sql

  • 另外:強制執行最低權限
  • 另外:執行白名單輸入驗證做爲輔助防護

不安全的例子:數據庫

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語句示例

如下代碼示例使用PreparedStatementJava的參數化查詢實現來執行相同的數據庫查詢。

// 必定要驗證
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_datareaderdb_datawriterdb_owner在存儲過程開始使用以前,DBA會根據要求爲webservice的用戶提供db_datareader或db_datawriter權限。可是,存儲過程須要執行權限,默認狀況下該角色不可用。用戶管理已集中在一些設置,但僅限於這3個角色,致使全部Web應用程序在db_owner權限下運行,所以存儲過程能夠正常工做。固然,這意味着若是服務器遭到破壞,攻擊者擁有數據庫的徹底權限,之前他們可能只具備讀訪問權限。

安全Java存儲過程示例

如下代碼示例使用CallableStatementJava的存儲過程接口實現來執行相同的數據庫查詢。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支持兩種轉義模式:

  1. ANSI_QUOTES SQL模式,以及這個關閉的模式,咱們稱之爲
  2. 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只能包含數字數字和字母af,歷來沒有任何特殊字符,它可能會使SQL注入。

在PHP中轉義SQLi

使用預準備語句和參數化查詢。這些是由數據庫服務器與任何參數分開發送和解析的SQL語句。這樣攻擊者就沒法注入惡意SQL。

你基本上有兩個選擇來實現這個目標:

  1. 使用PDO(適用於任何支持的數據庫驅動程序):
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
$stmt->execute(array('name' => $name));
foreach ($stmt as $row) {
    // do something with $row
}
  1. 使用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應用程序的數據庫用戶都無權訪問表自己。

相關文章
相關標籤/搜索