[目录]
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中实现:encod
value);
第二种方法使用反斜线编码,但PostgreSQL还需在字符串字面量前放置一个大写的E字母:select * from user where LastName=E'O\'Welly';
PHP中可以使用add_slashes()或str_replace()对反斜线编码(not so good method)。对于使用PostgreSQL的PHP代码,应该使用 encod
value); 该函数将调用PQescapeString()方法 ,它的操作: ' => '' \ => \\
PostgreSQL中还可以采用其他办法创建字符串字面量——使用字符,
字符允许开发人员在SQL语句中使用类似标记(tag-like)的功能:
select * from user where LastName = q
O'Welly$quote;
这种情况下,对于用户输入的任何一个字符,都需要确保使用一个反斜线进行转义处理:
encodevalue = str_replace("","
", $value);
5.5 防止NoSQL注入
在NoSQL查询的API中,绝大多数方法都提供将数据与代码清晰分离的方法,eg: PHP中使用MongoDB时,典型方法是使用关联数据插入数据:
users−>insert(array
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