Nodejs SQL注入攻击与防护

390 阅读13分钟

什么是SQL注入(SQL Injection)

SQL注入是一种常见的Web应用攻击方式。但SQL是一种关系型数据库相关的技术,怎么会成为Web应用系统的一个安全隐患呢?那是因为非常多的Web应用的模型层,就是建构在关系数据库系统之上的原因,也就是说,关系型数据库已经成为一种主流的Web应用架构的一部分,Web应用需要通过SQL语言来对数据进行操作。SQL注入攻击的本质,就是利用了基于SQL语言作为编程机制的关系性数据库系统的这一结构化和逻辑化的缺陷,通过有针对性的改写参数输入,来改写原有语句(SQL指令)的目的和语义,达到实现网络攻击的目的。

和普通人的认知相反,从原理上而言,SQL注入不是什么非常复杂、高深和稀有的技术,而是一种简单、常见、广泛而又容易实施的攻击方式。在一个相当长的阶段中,SQL注入成为一种相对主流的网络安全问题和攻击方式,可能是由于以下的原因:

  • SQL数据库是普通网络应用建构的事实标准,而被广泛使用
  • SQL数据库中的数据和信息,通常是业务系统的核心,特别是账号和重要业务数据,其攻击价值较高
  • SQL语言相对是标准化程度比较高的技术,很多不同的数据库系统都使用相同的SQL语句和处理方式
  • 基于SQL的Web应用通常是开放或者半开放的系统,通过网络进行攻击比较容易实现
  • SQL注入攻击内置在业务系统的正常网络操作之中,像防火墙、协议过滤等防护手段是无效的
  • 早期和现在的很多应用系统,使用字符串拼接的方式来构造和处理SQL语句,给参数注入攻击留下了漏洞,而且这个漏洞不能从逻辑上进行修复,否则会影响业务系统正常运行;而要修复它,需要进行程序代码级别的改写

随着互联网技术和安全的发展,特别是网络安全在IT行业和开发者当中的认证和意识的不断增强,大家在系统建设中也愈来愈自觉的有意识的遵循信息安全的规范和要求,这一问题和风险已经得到了明显的改善。注入攻击曾经长期在权威的OWASP Top 10 安全风险列表高居榜首,但最近的排名已经有所下降,在最新的2023年中滑退到的第三名,就可以说明这一趋势(图为OWASP TOP 10 (2017-2023) 排名变化表)。

mapping.png

当然,SQL注入还在这个主要风险列表当中,而且和系统的开发和建设工作关系紧密,对系统的整体安装有重大的影响,也是非常值得我们重视和研究的。

攻击的目的

执行和实施SQL注入攻击的目的可以有很多种。比如:

  • 获取信息

收集和获取信息,是很多网络攻击的很重要的准备工作,它的目的可能并不是直接的控制系统、窃取数据或者破坏操作,而是首先尽可能的获取当前系统、应用程序包括数据库的相关信息,然后对这些数据进行分析,为后续的攻击做好准备。

  • 获取非授权或范围扩大的数据

一般情况下,使用SQL进行数据查询的时候,都是使用一些限制条件来对结果集进行过滤,达到数据访问控制的目的。如果攻击者可以通过注入攻击,使条件过滤的机制失效,就可以访问到原本无权访问的数据,从而造成非授权信息的泄露。

  • 扩展授权

默认理想情况下,应用服务当前连接数据库所使用的账号,权限是被限制在当前的数据库当中的。但可能由于错误的配置,或者仅仅是数据库管理员的懒惰,他可能将其配置为超级用户或者管理员权限。

这样,通过注入攻击,攻击者可以通过这个账号可能会有权访问其他数据库或者系统数据库,造成系统或者业务相关信息的泄露。

  • 破坏或删除数据

SQL注入攻击不见得一定要达到身份伪造或者偷窃信息的目的,有的就是纯粹的想要破坏或者清除业务系统中的数据,达到业务系统无法正常工作的目的。

常见的SQL注入攻击方式

SQL注入的底层逻辑其实非常简单,注入(Injection)这个词的描述就非常精准恰当。就是在原有的SQL语句当中,插入了某些恶意的语句,这样在数据库系统执行时,无法从语义上分别原始文本和注入后的文本,从而无差别的执行,改变了原始SQL的语义和目的,达到攻击的效果。

常见的SQL注入攻击,从SQL构造的模式而言,可以大致分为下面三种类型:

  • 真值条件攻击(True Value)

通过构造永远为真的SQL查询条件的语句片段,来重构SQL的条件语句从而绕过信息查询的条件过滤机制。看一个示例:

原语句(参数为 :1):

SELECT * FROM Users WHERE UserId = 1 ;

注入后的语句(注入参数为: " 1 or 1=1"):

SELECT * FROM Users WHERE UserId = 1 OR 1=1;

这样的查询就会得到这个表中所有的记录。扩大了数据和信息的范围,让攻击者可以得到或者收集本来无权访问的数据。

  • SQL语句栈攻击 (SQL Stack)

如果应用服务器允许一次执行多个SQL语句,就有可能通过构造多个SQL语句来执行攻击。比如:

原语句(参数为 :10 ):

SELECT * FROM products WHERE id = 10

注入后的语句(注入参数为 " 10; Drop members "):

SELECT * FROM products WHERE id = 10; DROP members

SQL语句栈攻击的核心在于通过注入";"号(通常在SQL语法中用于分隔多个语句,依次执行)。这样就可以在一个正常的语句中,扩展加入了攻击者想要的指令,从而达到攻击的目的。

  • 注释信息攻击(Comment) 攻击者通过构造注释信息,可以让一些过滤或者检查机制失效。比如:

原语句为:

SELECT * FROM health_records WHERE date = '22/04/1999' AND id = 33

注入后(改写参数)的为:

SELECT * FROM health_records WHERE date = '22/04/1999'; -- ' AND id = 33

这种攻击的方式是通过在参数中加入标准注释符号"--",让此参数后的语句无效,就可能改变查询的条件,达到扩大数据范围的目的。

上面可以看到,SQL注入,主要的攻击模式其实就是这么几种,但通过对数据库技术、实现和运行深入理解的攻击者们,在具体的实施细节方面,可谓是思路奔放,脑洞打开,精益求精,创造了各种匪夷所思的攻击方式,可以说是令人叹为观止。

比如这个,可以猜猜它是在做什么:

输入图片说明

下面的链接列举了更多攻击的操作和案例:

www.w3schools.com/sql/sql_inj…

www.netsparker.com/blog/web-se…

SQL注入攻击防护

了解了理解了各种上的注入攻击方式和其原理后,我们就可以有针对性的设计相关的防护机制,来对SQL注入攻击进行防护。这些方式包括了改善开发规范,进行输出检查,优化数据处理等等。

输入检查

首先应当对涉及到数据库操作的输入进行检查,提前发现和过滤不合理的输入信息。对前后端分离的Web应用系统来说,如果注重用户体验和系统性能的话,这个检查可以首先在前端进行。但最终,还是需要在接口上来执行(防止直接构造Web请求进行攻击的情况)。

一般情况下,业务系统使用的数据都是比较简单或者非常业务化的,具有一定的规则或者格式。我们也可以对业务处理工作进行分析,要求业务系统将可能的输入限制在一个相对严格的范围之内。然后就可以制定输入信息检查的策略和方式。

在输入检查处理中,需要特别注意结合常见的注入攻击模式,设计有针对性的检查方式,如在一个整数参数中,不应当出现"="号,只应该包括数值;还需要特别注意正常参数中不应当出现如 "update, delete, where, -- "等SQL语句所使用的关键词。当然这不是绝对的,需要结合业务特点进行排除。

使用参数化SQL语句

SQL本质上就是一个字符串,但业务的需求又让开发者不能事先明确的定义字符串的格式和内容。要使程序处理能够参数化来使用各种业务场景,我们会很自然的想到通过拼接的方式,来达到使SQL语句动态化的效果。根据上面讨论的SQL注入原理,这种方式显然具有很大的安全隐患。

针对这种情况,主流的SQL数据库系统,都提供了参数化查询和操作的机制。就是允许开发者将主SQL语句和执行参数分别提交到数据库系统进行执行,这样就可以单独的对参数进行检查和处理,而非需要考虑和分析整个SQL语句。这样可以明确的排除不应当出现参数内容中的文本,比如注释信息和条件判断等。这样就基本杜绝了几种常见的注入攻击作用的机制和方式。

在实际的处理中,数据库系统还可以以此对执行过程进行优化,比如预先编译好需要执行的语句,然后基于参数来执行,从而减少解析和准备的环节,提高处理效率和性能。

限制查询或操作结果

开发人员在开发时应当充分考虑业务特点,设计和优化数据操作。作为一个最佳实践,笔者建议在任何查询操作中,都考虑通过limit子句限制查询结果,而非全面依赖查询条件。这样做不但提高了安全性,还能够避免不可预料的查询结果,提高查询性能,可限定的查询结果,也可以优化数据库资源占用。

一个典型的应用常见就是进行用户认证的时候,需要查询用户信息,作为这个业务的场景,只需要查询一条记录,而且时非常明确的,这时就可以将查询限制为 limit 1。在能够优化查询过程的同时,显然这一设置可以避免很多方式的注入攻击。

数据修改的操作也是类似的道理。比如在修改数据的时候,可以先查询要进行修改的数据,然后在此基础上再进行数据的更新,这一也可以预先限定数据范围,提高数据安全性。

优化数据库设置和管理

其实在数据库系统层面上,也提供了很多的机制来抵御和防护注入攻击。这里简单的例举几条:

  • 使用视图来限制数据范围 对于特定的业务,可以使用视图来限制数据的记录和字段范围,来达到限制数据访问范围的目的。视图是只读的,注入攻击中的清除数据操作可能就无法达到目的。

  • 合理的为应用系统分配数据库账号

如果要共享数据库系统,则建议为每个业务和应用都分配不同的数据库账号,并设置合适的,支撑业务所需最小的权限;要非常谨慎的使用数据库超级用户或者管理员权限

  • 只读、查询类或者数据交换类的应用

这些应用通常都是只读的,应当为这些只读的业务操作,分配只读的数据库账号,并设置合适的权限,保证此账号不能对数据或者数据库结构进行修改

  • 合理部署和设置数据库访问

数据库系统不能直接暴露在公开网络之中,而且应该设置为有应用系统可以通过网络访问。这一点和注入攻击防护关系不大,但却是一项重要的Web应用安全设置原则。

当然,这些操作会额外的引入一些管理和运维的复杂性。需要充分评估安全需求和可操作性后进行实施。

Nodejs中的实践

所有的注入攻击,其实都需要通过某个Web应用系统或者机制来进行,因为前端应该不会直接来连接数据库系统并且执行SQL语句。它们的执行机制应该也是类似的,就是基于前端的请求,通过后端处理,来构造SQL语句,连接数据库来进行执行。下面以一个nodejs应用系统,来进行说明。

本文并不是想深入讨论nodejs中SQL开发编程的问题,只是借助这个应用系统阐述SQL注入防护的机制,所以先简化一下场景。 我们在nodejs应用中,已经可以连接了数据库系统(pdb),Web客户端的请求也已经处理,请求的参数已经封装到了qparam中,并以一个用户登录时执行的查询作为示例:

使用SQL拼接的方式,相关的代码可能如下:

// 构造查询SQL let sql = "select id,name where login = '" + qparam.login + "' and passwd = "'" + qparam.passwd + "'";

// 查询符合条件的记录 let queryResult = await pdb.query(sql);

// 后续处理...

这时,如果进行注入攻击,可以使用 qparam.login = "admin' or 1= 1",就可以获得admin的记录并通过验证。这时候passwd的内容和形式已经无关紧要,达到了绕过条件检查的目的。

因此,改进后的处理过程和代码应该是:

// 查询sql let sql = "select id,name,passwd where login = $1 limit 1";

// 查询符合条件的记录 let queryResult = await pdb.query(sql,[qparam.login]);

// 检查查询结果 if (queryResult?.length > 0) { let user = queryResult[0]; // 检查密码 if (user.passwd != qparam.passwd) { // 密码错误 } else { // 登录成功 ... } } else { // 未找到记录 }

这里有几个安全性的要点:

  • 使用参数化进行SQL执行
  • 限制查询结果,可以确定只有一条用户记录需要处理
  • 查询密码内容进行比较处理,而非直接密码的检查,可以抵御密码注入

总结

SQL注入的攻击和防护并不深奥,主要是理解其攻击的原理,就可以有针对的设计和实施防护方案。当然我们也要与时俱进,经常关注新的网络应用安全趋势和技术发展,并将其运用在系统架构和运维的实践当中,才能持续提高和改善系统安全,保证系统长期安全稳定运行。