web安全 - SQL注入漏洞

396 阅读4分钟

SQL注入漏洞是Web层面最高危的漏洞之一。

SQL注入原理

下图是一个登录表单,输入正确的账号和密码后,页面会跳转到详情页;否则,提示账号或密码错误。

login.gif

现在我们使用一个比较特殊的用户“' or 1=1 -- ”,密码随意设置,发现也是可以正常登录的,如下图所示。

sql-injection.gif

为什么随意输入密码也可以进入详情页?进入数据库中查找,也没有“' or 1=1 -- ”这个用户。难道是程序出现BUG了吗?下面我们来详细分析程序,看看问题出现在哪里?

经过分析发现,登录请求到达服务器之后,会调用下述的方法处理:

async function login(ctx, next) {
  const {
    username, password,
  } = ctx.request.body

  const sql = `select count(*) from users where username='${username}' and password='${password}'`

  const exists = await db.query(sql)
    .then((results) => {
      return results[0] && results[0]['count(*)']
    })

  ctx.status = exists ? 200 : 401
  ctx.body = {
    message: exists ? 'Login successful' : 'Invalid username or password',
  }

  return next()
}

我们第8行代码处插入断点,重新使用账号“' or 1=1 -- ”登录并跟踪SQL语句,发现最后执行SQL语句为:

select count(*) from users where username='' or 1=1 -- ' and password='111111'

此时的password已经被注释了,而且username='' or 1=1永远为真。很显然,返回的数据条目大于0,所以可以顺利通过认证,登录成功。

这就是一次简单的SQL注入过程,如果不加以防范,可能会对系统造成巨大的危害。比如,在账号处输入:

“' or 1=1; drop table users -- ”

如果此时开启了多语句执行功能,这里会直接删除users表。

由此可知,SQL注入漏洞的形成原因就是:用户输入的数据被SQL解析器执行

防止SQL注入

SQL注入攻击问题最终归于用户非法输入被“准确地”送达后端,并被SQL解释器“准确地”执行。所以,要有效的防护SQL注入,我们需要从代码上入手,避免直接拼接SQL查询语句。

输入数据校验

对用户输入的数据进行校验。比如:

  • 限制账号只能用字母、数字、下划线组成。
  • 对数据类型进行严格校验

特殊字符转义(不推荐)

使用\对特殊字符进行转义,防止恶意字符(如')对SQL语句造成破坏。例如,用户输入账号为“' or 1=1 -- ”,转义后SQL语句如下:

SELECT count(*) from users where username='\' or 1=1 -- ' and password=111111

这样,转义后的输入不会干扰SQL查询的结构,避免了 SQL 注入的直接执行。但是,转义并不能解决二次注入的问题。

所谓的二次注入,即攻击者在一个查询中注入恶意代码,并将其存储在数据库中。这个数据本身可能是无害的,但在系统的后续查询中,它被再次使用,从而导致恶意代码执行。

假设有一个注册表单,系统对账号进行转义。当用户输入“aric' or 1=1 -- ”时,即使对输入数据做了转义处理(“aric\' or 1=1 -- ”),但这仍是个恶意数据,可能在后续的查询中被利用。例如,在用户登录时,系统执行了下述查询:

SELECT * FROM users WHERE username = 'aric' or 1=1 -- ' AND password = '';

它依然会被执行,从而能够绕过身份认证。

参数化查询 / 预处理语句

是防止 SQL 注入的最有效方法。它不仅自动处理输入的转义,还能确保每个输入都是作为数据传递给数据库,而不是作为查询的一部分。

例如,在mysql2中使用:

const login = async (ctx, next) => {
  const {
    username, password,
  } = ctx.request.body

  const sql = `select count(*) from users where username=? and password=?`

  const exists = await db.query(sql, [username, password])
    .then((results) => {
      return results[0] && results[0]['count(*)']
    })
    
  more...
}

使用ORM

许多现代框架和库提供 ORM(对象关系映射)功能,它们通过封装 SQL 查询和数据库操作,自动处理 SQL 注入的防护工作。ORM 提供了更高层次的抽象,减少了手动拼接 SQL 查询的机会。

例如,Node.js的一个ORM库Sequelize

使用存储过程

存储过程可以将SQL查询封装在数据库内部,减少外部对 SQL 查询的直接操作。使用存储过程时,用户输入的内容不会直接插入到SQL语句中,从而减少了SQL注入的风险。

安全配置

  • 禁止多语句执行
  • 数据库用户权限管理

通过采取这些措施,可以有效防止 SQL 注入攻击,保护数据库免受未授权访问。