sql注入之代码层防御策略

0x00 前言

之前写sql注入的博客都是站在攻击者角度上的,这次站在防御方的角度上,总结一下代码层防御sql注入的一些方法。

0x01 代码层防御

在易受sql注入攻击的应用程序开发过程中,如果我们在代码里面进行一些合理的过滤和安全措施,就可以防范绝大多数的sql注入。

0x02 参数化语句

引发sql注入最根本的原因是将sql语句创建成字符串发送给数据库,即动态sql。

而现在大多数程序在访问数据库时已然采用了参数化语句的方式,即用占位符或者绑定变量来向sql查询提供参数,而非直接让用户使用sql语句。显然,这种方式提升了安全性。

我们给出一段代码来说明参数化:

1
2
3
4
5
6
7
8
9
$servername = "localhost";
$username = "root";
$password = "xxxxx";
$dbname = "xxxxx";
$conn = new mysqli($servername, $username, $password, $dbname);
$username = request("username");
$password = request("password");
sql = "select * from user where username = '".$username."'and password = '".$password."'";
$result = $conn->query($sql);

我们可以看到,这是如今大多数后台都会采用的书写方式,他比起直接让用户控制sql语句减少了一定的风险,但是在之前写的渗透测试方式中,仍然有无数种方法成功注入。所以我们还得继续加一层保险——对参数进行输入验证。

0x03 php中输入验证

我们首先看一下在php中怎么进行输入验证。
php中我们能用到的函数如下:

  • preg_match(reg,match): 使用正则表达式reg对match字符串进行正则匹配。
  • is_(input): 检查输入input是否为,如检查数字is_numeric()。
  • strlen(input): 检查输入的长度。

其中preg_match如果用/i可以匹配大小写不敏感的数据,所以大小写混用也是无法绕过的。

假如黑名单过滤,我们只要匹配出任意违法字符,就禁止入库并且返回违法输入的提示。

1
2
3
4
5
6
7
$username=$_POST['username'];
if(preg_match("/*|#|;|,|is|file|drop|union|select|ascii|mid|from|(|)|or|\^|=|<|>|like|regexp|for|and|limit|file|--|||&|".urldecode('%09')."|".urldecode("%0b")."|".urldecode('%0c')."|".urldecode('%0d')."|".urldecode('%a0')."/i",$username)){
die('illegal input!');
}
else{
echo "success!";
}

再比如白名单过滤,我们可以仅允许数字字母,如果有超出数字字母的字符使匹配失败,就禁止入库并且返回违法输入的提示。

1
2
3
4
5
6
7
8
$username=$_POST['username'];
if(!preg_match("/^[a-z\d]*$/i",$username))
{
die('illegal input!');
}
else{
echo "success!";
}

0x04 java中输入验证

Java中的输入验证支持专属于正在使用的框架,如下是使用构建Web应用的框架JSF(Java Server Faces)对输入验证提供支持的示例代码,定义了一个输入验证类,实现了javax.faces.validator.Validator接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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中以便启用上述验证器:

1
2
3
4
<validator>
<validator-id>namespace.UsernameValidator</validator-id>
<validator-class>namespace.package.UsernameValidator</validator-class>
</validator>

然后在相关JSP文件中引用在faces-config.xml中添加的内容:

1
<h:inputText value="username" id="username" required="true"><f:validator validatorId="namespace.UsernameValidator" /></h:input>

0x05 .net中输入验证

ASP.NET提供了很多用于输入验证的内置控件,其中最有用的是RegularExpressionValidator控件和CustomValidator控件,下面示例代码是RegularExpressionValidator验证用户名的例子:

1
2
3
<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验证口令是否为正确格式的示例:

1
2
3
<asp:textbox id="txtPassword" runat="server"/>
<asp:CustomerValidator runat="server" Controlvalidate="txtPassword" CLientValidationFunction="clientPwdValidate"
ErrorMessage="Password does not meet the requirements." onServerValidate="PwdValidate">

0x06 html中输入验证

在写html输入验证之前先强调一点,html中的验证是基于前端的,而基于前端的验证就是客户端可以在浏览器直接更改的,所以他是不可靠的,没有实际作用的。在html验证之后,一定还要在后端重新验证。

但是为什么还要提他呢?因为在很多情况,如果我们过滤十分严格,输入一些敏感参数的人并不一定都是攻击者,这个时候每条语句都拿到后端判断然后跳转十分浪费时间,所以在前端给出一定的限制和提示,就能提升正常用户的体验。

1
<input type="text" required="required" patter="^[a-zA-Z\d]*" ...>

0x07 编码输出

我们都清楚,白名单过滤要比黑名单过滤安全的多,在能使用白名单的时候尽量使用白名单,但是很多时候业务需求不能让我们使用白名单,这个时候我们就要使用黑名单结合编码的方式,提升安全性。

我对其他几个语言不是非常熟悉,这里就只介绍一下php的编码函数。

这些编码函数,能够将允许范围内的违法字符编码后再存入数据库,能一定程度的防止我们构造语句逃逸,提升了安全性。

但是这些函数没有用对,仍然会出现一些问题,接下来我分享一下上图中我所了解的一些编码函数会存在的安全问题:

  • 其中htmlspecialchars() 这个函数对xss的过滤十分有效,但是直接使用它时,它只会转义<>符号,对sql注入是没有任何防御效果的,只有加入ENT_COMPAT参数过滤单引号或者加入ENT_QUOTES参数过滤单、双引号,才能用在sql注入的防御中。

  • 其中addslashes()函数,如果在设置了编码的页面如“set character_set_client=gbk” ,就可能造成宽字节注入,在编码时将反斜杠吃掉。

  • 其中addslashes()函数和urlencode()或者一些其他的编码函数如big5码的编码函数组合使用时,就可能造成二次注入,用编码产生新的单引号。

目前我最常见到的就是这几种,以后遇到新的还会及时补充。

0x08 总结

开发人员在代码层稍微多费一点时间增加一些函数,就可以抵御绝大多数的攻击,所以在代码层做防御是十分必要的,但是并不是说做了一些措施就一定能高枕无忧。要想提升整个系统的安全性,需要的是多种技术多个层面的防御相互结合。