SQL注入攻与防之代码层防御SQL注入

[目录]

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 输出编码

 

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 输出编码

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。