Web 安全漏洞之 SQL 注入

提醒:本文最后更新于 1952 天前,文中所描述的信息可能已发生改变,请谨慎使用。

Featured Image

什么是 SQL 注入

鲁迅

“有人的地方就有江湖,有数据库存在的地方就可能存在 SQL 注入漏洞。”

在所有漏洞类型中,SQL 注入可是说是危害最大最受大家关注的漏洞。简单说来,SQL 注入是通过在用户可控参数中注入SQL语法,破坏原有SQL结构,达到编写程序时意料之外结果的攻击行为。还是以 ThinkJS 为例,假设我们写了如下一个接口(实际情况肯定不会这么写的):

// user.js
module.exports = class extends think.Controller {
  async loginAction() {
    const { username, password } = this.post();
    const user = await this.model().query(
      `SELECT * FROM user WHERE name = "${username}" AND password= "${password}"`
    );

    if (think.isEmpty(user)) {
      return this.fail();
    }
    return this.success(user);
  }
}

当用户提交的 usernameadmin"; -- 的话,最终执行的 SQL 语句就会变成

SELECT * FROM user WHERE name = "admin"; --" AND password= "111"

最终攻击者就可以成功登录 admin 账号了,这就是最简单的 SQL 注入了。从上面这个简单示例中,我们发现漏洞成因可以归结为以下两个原因叠加造成的:

  1. 程序编写者在处理应用程序和数据库交互时,使用字符串拼接的方式构造SQL语句。
  2. 未对用户可控参数进行足够的过滤便将参数内容拼接进入到SQL语句中。

SQL注入根据攻击者获取数据的方式分为回显注入报错注入以及盲注。刚才演示的直接从返回结果中获取数据则为回显注入,当然也可以通过 MySQL 执行的报错结果中嗅探到数据库的结构和内容,这就是报错注入。盲注则是根据数据库执行的延时等操作来判断是否接近正确值,简单的说来有点像是拿着听诊器试探保险箱的密码的感觉。

不同的分类原则会有不同的分类,也有按照注入位置及方式不同进行分类分为POST注入GET注入cookie注入盲注延时注入搜索注入base64注入等。不过大家都支持分类形式不同,原理还是一致的,这里就不一一细说了。

SQL 注入的危害

如果网站存在 SQL 注入漏洞,相当于将数据库直接暴露在攻击者面前,可想而知危害会有多大了。攻击者利用 SQL 注入漏洞能实现以下攻击:

  1. 跳过账户权限验证达到越权
  2. 获取数据库关键信息从而进行脱库
  3. 在特别情况下还可以修改数据库内容或者插入内容到数据库,如果数据库权限分配存在问题,或者数据库本身存在缺陷,那么攻击者可以通过SQL注入漏洞直接获取webshell或者服务器系统权限。

防御方法

数据校验

从文章开头可以看到,其实漏洞的主要原因还是没有对用户输入的数据进行过滤,所以对来自用户的数据(GET, POST, cookie 等)最好做到以下两种过滤校验

  1. 检查输入的数据是否具有所期望的数据格式。这种在参数是数字的时候特别有效,如果攻击者选择在参数中插入内容的话则会被转换成 NaN 导致攻击失败。在 ThinkJS 中我们提供了强大的 Logic 功能可以方便的对数据进行格式校验。
  2. 使用数据库特定的敏感字符转义函数把用户提交上来的非数字数据进行转义。在 ThinkJS 中封装了 escapeString() 方法可以对敏感字符进行转义,其原理则和 PHP 的 mysql_escape_string() 方法是一致的。

检查输入数据格式在 ThinkJS 中还能防止另外一种非通用 SQL 安全问题。文章开头的示例代码我们在实际的应用中一般会这么写:

// user.js
module.exports = class extends think.Controller {
  async loginAction() {
    const { username, password } = this.post();
    const user = await this.model('user').where({
      name: username,
      password
    }).find();

    if (think.isEmpty(user)) {
      return this.fail();
    }
    return this.success(user);
  }
}

当我们构造如 name=admin&password[]=!%3D&password[]= 的请求参数时,最终执行的 Model 语句就会变成

this.model('user').where({name: 'admin', password: ['!=', '']});

由于 HTTP 请求的自动合并数组的特性造成了我们的 SQL 语句并非是我们想要的效果。虽然说框架本身已经针对这种情况进行了处理,当用户输入参数被认为是 SQL 运算符时则会将关键字增加空格,从而将其变成普通字符串避免这个问题。不过这种方法会有 一定的损伤,毕竟当真的要传这几个运算符的情况的时候接收到的数据和请求的不一样还是有点懵逼的。所以最好还是在 Logic 层对数据进行完善的校验将问题前置比较好。

除了数据校验,也可以选择使用数据库的存储过程和预定义指针等特性来抽象数库访问,使用户不能直接访问数据表和视图。但这个办法又有别的影响。

via: SQL注入

权限限制

严格限制Web应用的数据库的操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害。**请记住永远不要使用超级用户或所有者帐号去连接数据库!**当数据库被攻击时将损伤限制在当前表的范围是比较明智的选择。通过权限限制可以防止攻击者获取数据库其它信息,甚至利用数据库执行 Shell 命令等操作。

日志处理

当数据库操作失败的时候,尽量不要将原始错误日志返回,比如类型错误、字段不匹配等,把代码里的 SQL 语句暴露出来,以防止攻击者利用这些错误信息进行 SQL 注入。除此之外,在允许的情况下,使用代码或数据库系统保存查询日志也是一个好办法。显然,日志并不能防止任何攻击,但定期审计数据库执行日志可以跟踪是否存在应用程序正常逻辑之外的 SQL 语句执行。日志本身没用,要查阅其中包含的信息才行。毕竟,更多的信息总比没有要好。

后记

综上所说之后,大家可能觉得 SQL 数据校验会比较麻烦,其实在 ThinkJS 中已经将关键字处理类的方法已经集成,使用程序提供的 ORM 方法进行 SQL 构造会比自己写 SQL 语句拼接来的更方便,同时也能提高项目代码复用,减少潜在的风险。如果对 ThinkJS 默认的 think-model 不喜欢的话,也可以使用其它第三方的 ORM 框架,例如 think-sequelize

参考资料:

Avatar
怡红公子 擅长前端和 Node.js 服务端方向。热爱开源时常在 Github 上活跃,也是博客爱好者,喜欢将所学内容总结成文章分享给他人。

0 评论