[目錄]
0x0 前言
0x1 領域驅動的安全
1.1 領域驅動的設計
1.2 領域驅動的安全示例
0x2 使用參數化查詢
2.1 參數化查詢
2.2 Java中的參數化語句
2.3 .NET(C#)中的參數化語句
2.4 PHP中的參數化語句
2.5 PL/SQL中的參數化語句
0X3 移動應用中的參數化語句
3.1 iOS應用程序中的參數化語句
3.2 Android應用程序中的參數化語句
3.3 HTML瀏覽器中存儲的參數化語句
0x4 輸入驗證
4.1 白名單
4.2 黑名單
4.3 Java中的輸入驗證
4.4 .NET中的輸入認證
4.5 PHP中的輸入驗證
4.6 在移動應用程序中檢驗輸入
4.7 在HTML5中檢驗輸入
0x5 編碼輸出
0x6 規范化
0x0 前言
本文內容將介紹與SQL注入相關的安全編碼行為的幾個方面。首先討論了在使用SQL時動態構造字符串的方法,然后討論與輸入驗證相關的各種策略及與輸入驗證緊密相關的輸出編碼。本文還會討論與與輸入驗證直接相關的數據規范化,生成安全應用時可以使用的設計層考慮和資源。每一個話題都是整體防御策略的一部分,不應將其作為獨立實現的技術,而應該根據實際情況使用多種技術使應用免遭SQL注入攻擊。
0x1 領域驅動的安全
1.1 領域驅動的設計
領域驅動的安全(Domain Driven Security)是一種設計代碼的方法,使用這種方法設計可以避免典型的SQL注入問題。領域驅動的安全靈感來自於領域驅動設計,它試圖充分利用來自DDD(Domain Driven Design)的概念以提高應用程序的安全性, DDD的相關文章可以參考:
① Domain Driven Design and Development In Practice
② Security in Domain-Driven Design
③ 領域驅動設計(Domain Driven Design)參考架構詳解 (推薦)
下圖是領域驅動設計的詳細架構:
圖1
1.2 領域驅動的安全示例
圖2中,通過將數據在應用程序的三個主要部分進行映射,創建了一個簡單的應用程序模型:
圖2
這里,對於用戶名的概念,似乎存在三種不同的隱含表示:① 瀏覽器中用戶名實現為一個字符串 ② 應用程序服務器端,用戶名是一個字符串 ③ 數據庫中,應用程序實現為某種類型。查看右側的數據映射,雖然左側從admin的映射看起來是正確的,但右側的映射使用一個完全不同的值作為結束(來自瀏覽器輸入)
對於一般的登錄情況而言,如果使用的SQL語句為:String sql = "select * from user where username='" + username +"' and password='" + password +"' ";
在這樣的代碼中,用戶名和密碼都是隱含概念,DDD的概念是只要一個隱含概念導致了問題,就應該使之成為一個顯式概念並引入一個類(在需要使用這些概念的地方使用這些類)。
在Java中,可以創建Username類,使之成為一個顯式的概念:
public class Username { private static Pattern USERNAME_PATTERN = Pattern.compile("^[a-z]{4,20}$");
private final String username;
public Username(String username) {
if(!isValid(username)) {
throw new IllegalArgumentException("Invalid username: " + username);
}
this.username = username;
}
public static boolen isValid(String username) {
return USERNAME_PATTERN.matcher(username).matches();
}
}
這個類中,對原始字符串進行了封裝,並在該對象的構造函數中執行了輸入檢驗——代碼中不可能創建一個包含無效用戶名的UserName對象, 簡化了在代碼其他查找輸入檢驗代碼的步驟。如果將輸入驗證和顯式概念應用在映射圖中,則映射關系如圖3所示:
圖3
0x2 使用參數化查詢
2.1 參數化查詢
引發SQL注入最根本原因之一是將SQL查詢構建成字符串(動態字符串構造),然后提交給數據庫執行。更安全的動態字符串構造方法是使用占位符或綁定變量來向SQL查詢提供參數(而非直接對用戶參數進行操作)。使用參數化查詢可以避免很多常見的SQL注入問題,另外,由於數據庫可以根據提供的預備語句來優化查詢,使用參數化查詢還能提高數據庫查詢的性能。
參數化查詢雖然可以很大程度解決動態拼接導致的SQL注入問題,參數化語句也是一種向數據庫提供潛在非安全參數的方法,通常作為查詢和存儲過程調用。它們不會修改傳遞給數據的內容,但如果正在調用的數據庫功能在存儲過程或函數中使用了動態SQL,依舊可能出現SQL注入。此外,還需要考慮到存儲在數據庫中的惡意內容之后可能在應用的其他地方被使用,這將導致應用在那時受到SQL注入(二階注入)。因此,參數化查詢的確可以解決SQL注入的問題,但是一般情況下應用程序的代碼並不是全局執行參數化查詢,因而留下來SQL注入的潛在風險,這也是本文后面需要講到輸入驗證和輸出驗證的原因。
看一個容易受到SQL注入攻擊的示例偽代碼:
Username = request("username"); Password = request("password"); Sql = "select * from users where username='"+ Username +"' and password='"+ Password +"'"; Result = Db.execute(Sql); If(Result) ...
使用動態拼接,直接將用戶輸入帶入數據庫中查詢,因此存在SQL注入
接下來會展示如何使用占位符進行參數化查詢,但是在此之前需要提醒一下:在一個SQL語句中並不是所有內容都可以參數化的,只有數據值是可以參數化的,對於SQL標識符或關鍵字則是不行的,eg. select * from ? where username = 'john';
一般來說,如果嘗試以參數方式提供SQL標識符,則應該首先查看SQL以及訪問數據庫的方式,之后再查看是否可以通過固定的標識符來重寫該查詢。
2.2 Java中的參數化語句
Java提供了JDBC框架(java.sql和javax.sql)作為獨立於供應商的數據庫訪問方法,支持多種數據庫訪問方法,包括使用PreparedStatement類使用參數化語句。
下面是使用JDBC預編譯語句的示例代碼(添加參數時,使用不同的set<type>函數如setString指定占位符的編號位置,從1開始):
Connection con = DriverManager.getConnection(connectionString); String sql = "select * from users where username=? and password=?"; PreparedStatement lookupUsers = con.PrepaeredStatement(sql); lookupUser.setString(1,username); lookupUser.setString(2,password); rs = lookupUser.executeQuery();
J2EE應用中,除了使用JDBC框架,還可以使用附加的包來高效訪問數據庫,eg. 持久化框架Hibernate
下面展示了如何使用代碼命名參數的Hibernate:
String str = "select * from users where username=:username and password=:password"; Query lookupUsers = session.createQuery(sql); lookupUsers.setString("username",username); lookupUsers.setString("password",password); List rs = lookipUsers.list()
接下來是在Hibernate的參數中使用JDBC風格的?占位符(參數編號從0而不是1開始)
String str = "select * from users where username=? and password=?"; Query lookupUsers = session.createQuery(sql); lookupUsers.setString(0,username); lookupUsers.setString(1,password); List rs = lookipUsers.list()
2.3 .NET(C#)中的參數化語句
.NET應用程序可以使用ADO.NET框架參數化語句,一共提供了四種不同的數據庫連接程序:用於SQL Server的System.Data.SqlClient、用於Oracle的System.Data.OracleClient、用於OLE DB的System.Data.OleDb和用於ODBC的數據源的System.Data.Odbc
ADO.NET數據提供程序以參數命令語法:
----------------------------------------------------------------------------------------------------------------
數據提供程序 參數語法
System.Data.SqlClient @parameter
System.Data.OracleClient :parameter(只能用於參數化的SQL命令文本中)
System.Data.OleDb 帶問號占位符(?)的位置參數
System.Data.Odbc 帶問號占位符(?)的位置參數
-----------------------------------------------------------------------------------------------------------------
① SqlClient實現的參數化語句
SqlConnection con = new SqlConnection(ConnectionString); string Sql = "select * from users where username=@username" +"and password=@password"; cmd = new SqlCommand(sql,con); cmd.Parameters.Add("@username",SqlDbType.NVarChar,16); cmd.Parameters.Add("@password",SqlDbType.NVarChar,16); cmd.Paramaters.value["@username"] = username; cmd.Paramaters.value["@password"] = password; reader = cmd.ExecuteReader();
②OracleClient實現的參數化語句
OracleConnection con = new OracleConnection(ConnectionString); string Sql = "select * from users where username=:username" + "and password=:password"; cmd = OracleComand(Sql, con);
cmd.Parameters.Add("username",OracleType.Varchar,16); cmd.Parameters.Add("password",OracleType.Varchar,16);
cmd.Paramaters.value["username"] = username;
cmd.Paramaters.value["password"] = password;
reader = cmd.ExecuteReader();
③ OleDbClient實現的參數化語句
OleDbConnection con = new OleDbConnection(ConnectionString); string Sql = "select * from users where username=? and password=?"; cmd = new OleDbCommand(sql,con); cmd.Parameters.Add("@username",OraDbType.VarChar,16); cmd.Parameters.Add("@password",OraDbType.VarChar,16); cmd.Paramaters.value["@username"] = username; cmd.Paramaters.value["@password"] = password; reader = cmd.ExecuteReader();
2.4 PHP中的參數化語句
PHP有三種用於數據庫訪問的框架,訪問MySQL的mysqli包,PEAR::MDB2包及PDO(PHP Database Object)
① mysqli包適用於PHP5.x,可以訪問MySQL 4.1+的版本
$con = new mysqli("localhost","username","password","dbname"); $sql = "select * from users where username=? and password=?"; $cmd = $con->prepare($sql); $cmd->bind_param("ss", $username, $password); $cmd -> execute();
② PHP使用PostgreSQl數據庫
$result = pg_query_params("select * from users where username=$1 and password=$2", Array($username, $password));
//開發人員可以在同一行代碼提供SQL查詢和參數
③ PEAR::MDB2支持冒號字符參數和問號占位符兩種方式定義參數
$mdb2 = & MDB2::factory($dsn); $sql = "select * from users where username=? and password=?";
$types = array('text','text');
$cmd = $mdb2->prepare($sql, $types, MDS2_PREPARE_MANIP);
$data = array($username, $password);
$result = $cmd->execute($data);
④ PDO是一個面向對象且獨立於供應商的數據層,支持冒號字符參數和問號占位符兩種方式定義參數
$sql = "select * from uses where username=:username and" + "password=:password"; $stmt = $dbh->prepare($sql); $stmt->bindParam(':username', $username, PDO::PARAM_STR,12); $stmt->bindParam(':password', $password, PDO::PARAM_STR,12); $stmt->execute();
2.5 PL/SQL中的參數化語句
PL/SQL支持使用帶編號的冒號字符來綁定參數:
declare username varchar2(32); password varchar(32); result integer; BEGIN Execute immediate 'select count(*) from users where username=:1 and password=:2' into result using username, password; END;
0X3 移動應用中的參數化語句
基於iOS和Android的設備都具有in-device的數據庫支持,並提供了創建、更新和查詢這些數據庫的API
3.1 iOS應用程序中的參數化語句
API通過SQLite庫libsqlite3.dylib支持SQLite,若直接使用SQLite(而非Apple的Core Data框架),則可以使用FMDB框架
可以使用executeUpdate()方法構建參數化的insert語句:[db executeUpdate:@"insert into artists (name) values (?)", @"balabala"];
同樣,查詢數據庫則使用executeQuery()方法:FMResultSet *rs = [db executeQuery:@"select * from songs where artist=?",@"balabala"];
3.2 Android應用程序中的參數化語句
insert語句:
statement = db.compileStatement("insert into artists (name) values (?)");
statement.bind(1,"user-input");
statement.executeInsert();
Query(在SQLite-Database對象上直接使用query方法):
db.query("songs", new String[] {"title"}, "artist=?", new String[] {"singer-name"}, null, null, null); /* 3 null:group by->having->order by */
3.3 HTML瀏覽器中存儲的參數化語句
HTML5標准中可以使用兩種類型存儲—— Web SQL Databae和 Web Storage規范,瀏覽器中通常使用SQLite來實現,可以使用Javascript來創建和查詢這種數據庫
t.executeSql('select * from songs where artist=? and song=?', [artist, songname], function(t,data){...});
// t => transaction, SQL語句將在事物中執行 最后一個參數為回調函數,用於處理從數據庫返回的數據
Web Storage規范使用setItem()、getItem()、removeItem()等方法
0x4 輸入驗證
輸入驗證指測試應用程序接收到的輸入,以保證其符合應用程序中標准定義的過程。它可以簡單到將參數限制成某種類型,也可以復雜到使用正則表達式或業務邏輯來驗證輸入。
輸入驗證分為兩種,一種為白名單驗證,另一種為黑名單驗證。
4.1 白名單
白名單驗證只接收已經記錄在案的良好輸入的操作,在接收輸入並進一步處理之前驗證輸入是否符合所期望的類型、長度或大小、數字范圍或其他格式標准。
使用白名單時,應考慮:
已知的值:輸入的值是否提供了某種特征,可以查找這種特征已確定輸入值的正確與否;
數據類型:數字類型.v.s 數字? 正數.v.s 負數? ...
數據大小:字符串長度正確? 數字的大小/精度? ...
數據范圍:數字會上溢出/下溢出? 日期范圍?
數據內容:郵政編碼、特定的符號...eg. ^\d{5}(-d{4})?$
通常來說,白名單驗證比黑名單更強大,但對於存在復雜輸入的情況,或難以確定所有可能的輸入集合時實現起來會比較困難(eg. Unicode大字符集)
輸入驗證和處理策略:
使用白名單驗證以確保只接收符合期望格式的輸入;
客戶端瀏覽器上執行白名單機制,防止用戶輸入不可接受數據時服務器和瀏覽器之間的往返傳遞,同時要使用服務器端白名單驗證機制,因為瀏覽器端數據可以由用戶修改;
WAF層同時使用白名單和黑名單機制,提供入侵檢測/阻止功能和監視應用攻擊;
應用程序中始終使用參數化語句以阻止SQL注入攻擊;
在數據庫中使用編碼技術以便在動態SQL中使用輸入時安全地對其進行編碼;
在使用從數據庫中提取出來的數據時恰當地對其進行編碼;
可以考慮將輸入值與一個有效的值列表進行比較,如果輸入值不在列表中就拒絕該輸入,eg.
sqlstmt:= 'select * from foo where var like ''%' || searchparam || '%'';
sqlstmt:= sqlstmt || ' ORDER BY ' || orderby || ' ' || sortorder;
searchparam、orderby、sortorder都可以被注入利用, 但orderby為SQL標識符, sortorder則為一個SQL關鍵字
=> 考慮在數據庫前端使用函數檢查提供的參數值是否有效:
FUNCTION get_sort_order (in_sort_order varchar2) return varchar2 IS v_sort_order varchar2(10):= 'ASC'; BEGIN IF in_sort_order IS NOT NULL THEN select decode(upper(in_sort_order), 'ASC', 'ASC', 'DESC', 'DESC', 'ASC', INTO v_sort_order from dual); END IF; RETURN v_sort_order; END;
利用已知值進行檢測還可以使用間接輸入——服務器端不直接接收來自客戶端的值,客戶端呈現一個允許的值列表,並向服務器端提交選中值的索引。
4.2 黑名單
黑名單驗證機制值拒絕已記錄在案的不良輸入的操作,通過瀏覽器輸入的內容來查找是否存在已知的不良字符、字符串或模式。如果輸入中包含眾所周知的惡意內容,則會拒絕它。
使用黑名單驗證要比白名單弱,因為潛在的不良字符列表非常大,這會導致不良內容列表也很大,檢索起來慢且不全,而且很難及時更新這些內容。
雖然大多數情況推薦使用白名單,但對於無法使用白名單時,可以使用黑名單來提供有用的局部控制手段。因此,孤立的使用白名單和黑名單都不妥當,另外,還可以結合輸出編碼 以保證對傳遞到其他位置的輸入進行附加檢查,從而防止SQL注入等攻擊。
4.3 Java中的輸入驗證
Java中的輸入驗證支持專屬於正在使用的框架,如下是使用構建Web應用的框架JSF(Java Server Faces)對輸入驗證提供支持的示例代碼,定義了一個輸入驗證類,實現了javax.faces.validator.Validator接口。
public class UsernameValidator implements Validator { public void validate(FacesContent faceContext, UIComponent uiComponent, Object value) throws ValidatorException { // get the username and transform it to a string String username = (String) value; // build a regexp Pattern p = Pattern.compile("^[a-zA-Z]{8,12}$"); // match the user name Matcher m = p.matcher(username); if(!matchFound) { FacesMessage message = new FacesMessage(); message.setDetail("Invalid Input-- Must be 8-12 letters");
message.setSummary("Username invalid");
message.setServerity(FacesMessage.SERVERITY_ERROR);
throw new validatorException(message);
}
}
需要將以下內容添加到faces-config.xml中以便啟用上述驗證器:
<validator> <validator-id>namespace.UsernameValidator</validator-id> <validator-class>namespace.package.UsernameValidator</validator-class> </validator>
然后在相關JSP文件中引用在faces-config.xml中添加的內容:
<h:inputText value="username" id="username" required="true"><f:validator validatorId="namespace.UsernameValidator" /></h:input>
在Java中實現輸入驗證,還可以使用OWASP的ESAPI:https://code.google.com/p/owasp-esapi-java/downloads/list
4.4 .NET中的輸入認證
ASP.NET提供了很多用於輸入驗證的內置控件,其中最有用的是RegularExpressionValidator控件和CustomValidator控件,下面示例代碼是RegularExpressionValidator驗證用戶名的例子:
<asp:textbox id="userName" runat="server"/> <asp:RegularExpressionValidator id="userNameRegEx" runat="server" ControlToValidate="userName"
ErrorMessage = "Username must contain 8-12 letters." ValidationExpression="^[a-zA-Z]{8-12}$" />
下面的代碼是使用CustomValidator驗證口令是否為正確格式的示例:
<asp:textbox id="txtPassword" runat="server"/>
<asp:CustomerValidator runat="server" Controlvalidate="txtPassword" CLientValidationFunction="clientPwdValidate"
ErrorMessage="Password does not meet the requirements." onServerValidate="PwdValidate">
4.5 PHP中的輸入驗證
PHP不依賴於表示層,其輸入驗證機制與Java相同,專屬於所使用的框架,如不使用框架可以使用PHP中的函數作為構造輸入驗證的基本構造塊:
preg_match(regex, matchstring)、is_<type>(input) eg. is_numeric()、strlen(input)
使用preg_match驗證表單參數示例:
$username = $_POST['username']; if(!preg_match("/^[a-zA-Z]{8,12}$/D", $username) {...}
4.6 在移動應用程序中檢驗輸入
移動應用程序中的數據既可以存儲在遠程服務器上,也可以存儲在本地的應用中。兩種情況都需要在本地檢驗輸入,但對於遠程存儲的數據,還需要在遠程服務器端檢查輸入,因為我們無法保證另一端一定是實際的移動應用程序。可以使用兩種方法進行校驗: 使用僅支持期望數據類型的輸入域類型(filed type);也可以訂閱輸入域的change事件,當接收到無效輸入時由數據處理程序進行處理(eg. Android支持input filter概念)。
4.7 在HTML5中檢驗輸入
類似於移動應用程序,HTML5可以在瀏覽器本地存儲數據,也可以將數據存儲在遠程服務器,對於存儲在瀏覽器中的數據,可以使用Javas或HTML5的<input>輸入域進行檢查:
<input type="text" required="required" patter="^[0-9]{4}" ...>
updating...
0x05 編碼輸出
使用數據庫時的常見問題是對包含在數據庫中的數據的內在信任,數據庫中的數據在保存到數據庫之前不會經過嚴格的輸入驗證或審查(可能來自外部的源)。使用參數化查詢是導致這種情況的行為之一,它可以避免動態SQL來防止SQL注入,但它在使用時並未驗證輸入,所以存儲在數據庫中的數據可能包含來自用戶的惡意輸入。數據庫中出現不安全數據時,可能引發二階SQL注入,有可能導致XSS。
即使使用了白名單輸入驗證,發送給數據庫的內容也可能是不安全的,eg. O'Welly這樣的名稱是有效的,應該允許出現在白名單輸入驗證中使用。如果使用該輸入動態產生一個SQL查詢,則可能引發嚴重問題:String sql = "insert into table1 values('" + fname + "', '"+ lname +"' )";
雖然可以使用參數化語句來防止輸入:',''); drop table table1-- 這樣的語句造成威脅,但對於無法或不適用使用參數化語句的情況,有必要對發送給數據庫的內容進行編碼。這種方法的局限在於,每次在數據庫查詢中使用這些值時都要進行編碼。
5.1 針對Oracle的編碼
在Oracle中,可以通過使用兩個單引號替換單個單引號的方法實現編碼目的 => 單引號被當做字符串的一部分,而不是字符串結束符
sql = sql.replace("'","''");
上面的代碼會導致O'Welly變成O''Welly,如果將其保存到數據庫中,這個字符串則會被保存為O'Welly。由於在PL/SQL中需要為單引號添加引用符,因此需要使用兩個單引號替換單個單引號:sql = replace(sql, '''', ''''''); 為了讓它看起來邏輯性更強和更加清楚,可以使用字符編碼: sql = replace(sql, chr(39), chr(39) || chr(39));
對於其他類型的SQL功能,同樣有必要對在動態SQL中提交的信息添加引用符(eg. like子句),Oracle中如下的通配符在like子句中是有效的:
----------------------------------------------------------------------------------
字符 含義
----------------------------------------------------------------------------------
% 匹配0或多個任意字符
_ 精確匹配任意一個字符
----------------------------------------------------------------------------------
對於上面的字符示例,可以在查詢者定義轉義字符、在通配符前添加該轉義字符並使用ESCAPE子句在查詢中加以指定確保得到正確處理:
select * from users where name like ''a%; -- easyly to get attacked select * from users where name like 'a\%' escape '\'; -- more safe
在Oracle 10g R1+中,還存在另一種引用字符串的方法—— ''q"引用,格式為:q'[quote char]string[quote char]',引用字符可以是任何未出現在字符串中的單個字符,除非Oracle期望匹配括號。eg. q'(5%)'
Oracle 10g R2中引入了新的dbms_assert包(如果無法使用參數化查詢就使用dbms_assert來執行輸入驗證),它提供了7個不同的函數:ENQUOTE_LITERAL、ENQUOTE_NAME、NOOP(not recommend to use)、QUALIFIED_SQL_NAME、SCHEMA_NAME、SIMPLE_SQL_NAME、SQL_OBJECT_NAME
-- non-secure query execute immediate 'select ' || FIELD || 'from' || OWNER || '.' || TABLE;
-- same query but using dbms_assert
execute immediate 'select ' || sys.dbms_assert.simple_sql_name(FIELD) || 'from' || sys.dbms_asser.enquote_name(sys.dbms_assert.schema_name(OWNER),false) || '.' || sys.dbms_asser.qualified_sql_name(TABLE);
5.2 針對SQL Server的編碼
對於使用單引號結束字符串字面量值來說,SQL Server和Oracle沒有區別,在Transact-SQL中的替換為:set @enc = replace(@input, '''', ''''''),對應的字符編碼:set @enc = replace(@input, char(39), char(39) + char(39));
SQL Server中like子句的通配符:
----------------------------------------------------------------------------------
字符 含義
----------------------------------------------------------------------------------
% 匹配0或多個任意字符
_ 精確匹配任意一個字符
[] 位於指定范圍[a-d]集合中的任意單個字符
[^] 未位於指定范圍[a-d]集合中的任意單個字符
----------------------------------------------------------------------------------
對於需要在動態SQL的LIKE子句中使用這些字符的示例,可以使用"[]"來引用,只有%、_、[需要被引用:eg. sql = sql.replace("%", "[%]")
同樣可以定義轉義字符並加以指定:select * from users where name like 'a\%' escape '\';
T-SQL中將單引號編碼為雙引號時,要注意為目標字符串分配足夠的存儲空間,因為當村處置過長時,SQL Server就會截斷它,導致在數據庫級的動態SQL中出現問題。同樣的原因,執行編碼時考慮使用replace()而非quotename(),因為quotename()無法處理超過128個字符的字符串。
5.3 針對MySQL的編碼
MySQL中可以用兩個單引號替換單個單引號,也可以使用反斜線引用單引號:sql = replace("'", "\'");
PHP提供了mysql_real_escape()函數,該函數自動使用反斜線來引用單引號及其他潛在危險字符,eg. 0x00(NULL)、換行(\n)、回車(\r)、雙引號(")、反斜線(\)及 0x1a(Ctrl+Z):mysql_real_escape($user);
在存儲過程中,MySQL的替換如下:set @sql = replace(@sql, '\'', '\\\''); 或者: set @enc = replace(@input, char(39), char(92, 39));
5.4 針對PostgreSQL的編碼
PostgreSQL有兩種方法對單引號進行編碼,第一種與前面Oracle和SQL Server中采用的方法類似,PHP中實現:$encodeValue = str_replace("'","''", $value);
第二種方法使用反斜線編碼,但PostgreSQL還需在字符串字面量前放置一個大寫的E字母:select * from user where LastName=E'O\'Welly';
PHP中可以使用add_slashes()或str_replace()對反斜線編碼(not so good method)。對於使用PostgreSQL的PHP代碼,應該使用 $encodevalue = pg_escape_string($value); 該函數將調用PQescapeString()方法 ,它的操作: ' => '' \ => \\
PostgreSQL中還可以采用其他辦法創建字符串字面量——使用$字符,$字符允許開發人員在SQL語句中使用類似標記(tag-like)的功能:
select * from user where LastName = $quote$O'Welly$quote;
這種情況下,對於用戶輸入的任何一個$字符,都需要確保使用一個反斜線進行轉義處理:$encodevalue = str_replace("$", "\\$", $value);
5.5 防止NoSQL注入
在NoSQL查詢的API中,絕大多數方法都提供將數據與代碼清晰分離的方法,eg: PHP中使用MongoDB時,典型方法是使用關聯數據插入數據:
$users->insert(array("username"=>"$username", "password"=>"password"))
查詢則如下所示:
$user = $users->findOne(array("username"=> $username))
上面的例子類似於參數化的語句,可以防止SQL注入,到對於一些更高級的查詢,MongoDB允許開發人員使用$where關鍵字提交一個Javascript函數:
$collection-> find(array("\$where"=> "function() {return this.username.indexOf('$test') > -1}"));
0x6 規范化
輸入驗證和輸出編碼面臨的困難是:確保將正在評估或轉換的數據解釋成最終使用該輸入的用戶所需要的格式。避開輸入驗證和輸出編碼的常用技術是:將輸入發送給應用程序之前對其進行編碼,之后再對其進行解碼和解釋以符合攻擊者的目標,下表列出了編碼單引號可以使用的方法:
--------------------------------------------------------------------
表示 編碼類型
--------------------------------------------------------------------
%27 URL編碼
%2527 雙URL編碼
%%317 嵌套的雙URL編碼
%u0027 Unicode
%u02b9 Unicode
%ca%b9 Unicode
&apos HTML實體
' 十進制HTML實體
 十六進制HTML實體
%26apos 混合的URL/HTML編碼
--------------------------------------------------------------------
很難預測應用程序是否會按照我們的理解進行解碼,因此給攻擊者留下潛在的機會(應用層、應用服務器、WAF層...),因此需要考慮將規范化作為輸入驗證方法的一部分
6.1 規范化方法
對於不常見輸入,最容易實現的方法是拒絕所有不符合規范格式的輸入,通過白名單驗證時通常會默認采用該方法(不會接收用於編碼數據的字符:&、%、#)
如果無法決絕包含編碼格式的輸入,就需要尋找解碼方法或其他方法來保證接收到的數據的安全,一種比較可行的方法是只將輸入解碼一次,接下來如果數據中仍包含經過編碼的數據就拒絕。
6.2 適用於Unicode的方法
遇到像UTF-8這樣的輸入時,一種方法是將輸入標准化(使用定義好的規則集將Unicode轉換成最簡單的形式)。Unicode標准化與規范化的差別在於:根據使用規則集的不同,Unicode字符可能會存在多種標准形式,可以使用NFKC(Normalization Form KC)作為輸入驗證的標准化形式。
標准化操作將Unicode字符分解成有代表性的組件,之后按照最簡單的形式重組該字符。大多數情況下,它會將雙倍寬度及其他Unicode編碼在它所在的位置轉換成各自的ASCII等價形式。
Java:
normalized = Normalizer.normalize(input, Normalizer.Form.NFKC)
C#:
normalized = input.Normalize(NormalizationForm.FormKC);
PHP:
$normalized = I18N_UnicodeNormalizer::toNFKC($input, 'UTF-8'); // PEAR:I18N_UnicodeNormalizer
還有一種方法是首先檢查Unicode是有效的,然后將數據轉換成一種可預見的格式,eg. ISO-8859-1.
下表列出的正則可以匹配使用UTF-8編碼的Unicode的有效性:
---------------------------------------------------------------------------
正則表達式 描述
----------------------------------------------------------------------------
[\x00-\x7F] ASCII
[\xC2-\xDF][\x80-\xBF] 雙字節表示
\xE0[\xA0-xBF][\x80-xBF] 雙字節表示
[\xE1-\xEC\xEE\xEF][\x80-xBF]{2} 三字節表示
\xED[\x80-x9F][\x80-xBF] 三字節表示
\xF0[\x90-xBF][\x80-xBF]{2} PANEL 1 TO 3
[\xF1\xF3][\x80\xBF]{3} PANEL 4 TO 15
\xF4[\x80-x8F][\80-xBF]{2} PANEL 16
----------------------------------------------------------------------------
檢查完是有效的格式后,就可以將其轉換成可預見的格式,eg. UTF-8 => ISO-8859-1(Latin 1)
Java: String ascii = utf8.getByte("ISO-8859-1");
C# : byte[] asciiBytes = Encoding.Convert(Encoding.UTF8, Encoding.ASCII, utf8bytes);
PHP: ascii = utf8_decode($utf8string);
0x7 通過設計來避免SQL注入的危險
對於新開發的系統而言,使用良好的設計模式有利於從根源上防止SQL注入
7.1 使用存儲過程
使用存儲過程之所以能減輕或防止SQL注入,是因為大多數數據庫使用存儲過程時可以在數據庫層配置訪問控制——通過正確配置許可權限來保證攻擊者無法訪問數據庫中的敏感信息。此外,動態拼接要求的許可權限比應用程序嚴格需要的權限更大:動態SQL在應用程序中組裝,之后被發送給數據庫執行,因而數據庫中所有需要被應用程序讀取、寫入或更新的數據均需要能夠被用於訪問數據庫的數據庫用戶賬戶訪問到。
使用存儲過程,並且只分配必要的數據庫許可權限,有助於減輕SQL注入的影響——限制攻擊者只能調用存儲過程,從而限制了能夠訪問或修改的數據。由於SQL注入不僅能發生在應用層,還能發生在數據庫層,因此如果攻擊者將惡意語句寫入到存儲過程中,雖然訪問和修改數據受到限制,但是如果在后續的動態SQL中使用了該輸入,仍可能造成SQL注入。
7.2 使用抽象層
考慮如下做法:為表示、業務邏輯、數據訪問定義不同的層,從而將每一層的實現從總體設計中抽象出來。假設應用程序除了以數據訪問層方式訪問數據庫外,不存在其他訪問方式,且之后沒有使用數據庫層提供的動態SQL提供的信息,則基本不可能出現SQL注入。使用參數化語句來執行所有數據庫調用的數據數據訪問層是這種抽象層的一個很好的例子。
7.3 處理敏感數據
最后一種減輕SQL注入嚴重影響的技術是考慮數據庫敏感信息的存儲和訪問,通常攻擊者感興趣的信息包括用戶名、口令、個人信息及信用卡相關信息等,因此有必要對敏感信息進行附加控制:
口令:存儲每個用戶口令的salted哈希(SHA256、SHA512),salt與哈希口令分開保存(登錄時通過用戶提供的信息計算出來的加鹽哈希與數據庫中的哈希比較),如果用火狐忘記口令,則為他生成一個新的安全口令。
財務信息:符合PCI-DSS標准
存檔:如果為要求應用程序保存提交給它的所有敏感信息的完整記錄,就應該考慮每隔一段合理的時間就存檔或清除這些不需要的信息。
出於安全方面考慮,建議開發人員在為關鍵對象選取應用名稱時盡量避免明顯的對象名如:password、pwd、paasw,另外還可以考慮使用不明顯的表名和列名保存口令信息以增加攻擊難度
還可以考慮創建一個蜜罐,當有人嘗試從數據庫讀取數據時能收到警報:
--create honeypot table create table app_user.tblusers(is member, name varchar2(30),password varchar2(30)); --create a policy function to send an email to administrator --use another pattern to create function create or replace secuser.function get_cust_id { p_schema in varchar2, p_table in varchar2 } return varchar2 as v_connection UTL_SMTP.CONNECTION; begin v_connection := UTL_SMTP.OPEN_CONNECTION('mailhost.victim.com',25); UTL_SMTP.HELO(v_connection,'mailhost.victim.com'); UTL_SMTP.MAIL(v_connection,'app@victim.com'); UTL_SMTP.RCPT(v_connection,'admin@victim.com'); UTL_SMTP.DATA(v_connection,'WARNING! SELECT PERFORMED ON HONEYPOT'); UTL_SMTP.QUIT(v_connection); return '1=1'; -- show the entire table --assign the policy function to honeypot table exec dbms_rls.add_policy('APP_USER','TBLUSERS','GET_CUST_ID','SECUSER','','SELECT,INSERT,UPDATE,DELETE');
7.4 附加安全開發資源
http://cwe.mitre.org/top25/#Listing
https://www.owasp.org/index.php/Category:OWASP_Enterprise_Security_API