Skip to Content
全部文章Node服务端面试常考-从sql注入到服务器注入防范

面试宝典-从sql注入到服务器注入防范

什么是sql注入?

概念解释

官方的解释是:

数据库注入(Database Injection)是一种常见且危险的网络攻击手段,攻击者利用应用程序对用户输入处理不当的漏洞,将恶意的 SQL 或其他数据库查询代码插入到应用程序与数据库交互的输入字段中,从而改变原有的数据库查询逻辑,达到非法访问、修改、删除数据库中数据的目的。

通俗的讲,数据库注入就是指用户通过正常接口或者非法手段将一些非法的数据库语法插入到数据库查询中,从而改变原有的数据库查询逻辑,达到非法访问、修改、删除数据库中数据的目的。

示例

为了演示数据库注入的过程,我们选用使用面最广的sql注入来举例,现在我在nestjs中有如下一个service方法, 他的逻辑如下,通过外部传入参数,然后在方法内通过拼接的方式得到查询语句,并执行。

import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from './user.entity'; @Injectable() export class AppService { constructor( @InjectRepository(User) private readonly userRepository: Repository<User>, ) {} async findUserByUsernameUnsafe(username: string) { const query = `SELECT * FROM user WHERE username = '${username}'`; return await this.userRepository.query(query); } }
  • 正常情况 如果我们传入参数 wangqiang ,最终得到的sql语句是:

SELECT * FROM user WHERE username = 'wangqiang'

这是一条没有风险的sql语句。

  • 异常情况 但是如果别有用心的人,传入的参数是 ' OR '1'='1 ,最终得到的sql语句是:

SELECT * FROM user WHERE username = '' OR '1'='1'

这就是一条不符合我们预期的语句了,它改变了原有的数据库查询逻辑,直接把整个表的数据都拖走了

假如他传入的参数是 ' ; DROP TABLE user; -- ,最终得到的sql语句是:

SELECT * FROM user WHERE username = '' ; DROP TABLE user; -- '

这句语法执行后,你的表都玩没了。

危害

从上面的例子可以看出,对服务器的注入危害非常非常可怕,一旦被注入,完全可以胡作非为,是服务端必须严防死守的一个点。

防范手段有哪些

sql注入原理

我们可以看出来,攻击主要就是让一些动态执行的地方,执行了恶意的语法,从而改变原有的数据库查询逻辑,达到非法访问、修改、删除数据库中数据的目的。

防范手段

当我们知道攻击原理之后,防范切入点有2个:

  • 服务端对外界来源的数据一律做过滤检查
  • 对于动态拼接语法、动态执行代码的地方都要做过滤检查

sql注入本质上就是一种服务端注入的一种,常见的除了sql注入以外,nosql注入、shell注入、LDAP注入等等,所有的服务端注入发生都是因为字符串拼接形式的执行导致。

外部来源参数检查

首先,防范于未然的做法是,服务端对提交的参数做注入检查,如果有注入风险则不处理这批参数。

检查思路有两种

  1. 在请求入口做全局检测
  2. 在具体方法内做局部检测

全局检测就是添加拦截器,工作量小,但是精细化不够,容易误伤不需要注入检测的接口。

局部检测一般借助一些参数校验包,比如class-validator,它可以校验某一个参数是否符合你的要求,通常这种工作量大。

全局拦截器

在express、koa、nestjs这些框架中,都支持定义拦截器,可以在请求入口统一做检查。

这里检查的项目主要取决于你的项目中有哪些动态拼接、执行的代码类型,举个例子,如果你有sql数据库,你就要检查sql,你有nosql数据库你就要检查nosql的注入, 你有shell脚本执行,你就要检查shell脚本的注入,等等…

这里用nestjs 做示例,定义拦截器 injection.interceptor.ts

import { Injectable, NestInterceptor, ExecutionContext, BadRequestException } from '@nestjs/common'; import { Observable } from 'rxjs'; // 定义 SQL 注入检测正则表达式,这里只是举例,实际预防注入还有其他的关键词 const SQL_INJECTION_REGEX = /(\b(SELECT|UPDATE|DELETE|INSERT|DROP|ALTER|CREATE|EXEC|UNION|OR|AND)\b)/i; // 定义 shell 注入检测正则表达式 const SHELL_INJECTION_REGEX = /(\b(;|\||&|`|$|\(|\))\b)/; @Injectable() export class InjectionInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: () => Observable<any>): Observable<any> { const request = context.switchToHttp().getRequest(); // 检查查询参数 this.checkParams(request.query); // 检查请求体参数 this.checkParams(request.body); // 检查路由参数 this.checkParams(request.params); return next(); } private checkParams(params: any) { if (typeof params === 'object' && params !== null) { for (const key in params) { if (params.hasOwnProperty(key)) { const value = params[key]; if (typeof value === 'string') { // 要过滤多少种种类的注入,取决与你的系统需要防范的风险点有多少种 if (SQL_INJECTION_REGEX.test(value) || SHELL_INJECTION_REGEX.test(value)) { throw new BadRequestException('输入包含 SQL 或 shell 注入风险的内容'); } } else if (typeof value === 'object') { this.checkParams(value); } } } } } }

main.ts 中引入拦截器

import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { InjectionInterceptor } from './injection.interceptor'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.use(new SqlInjectionInterceptor().use); await app.listen(3000); } bootstrap();
局部校验

代码就不写了,我自己也没有用局部校验,都是用全局校验。如果是nestjs的话,大致思路就是:

  1. 创建校验装饰器
  2. 创建DTO,写上校验装饰器

防范数据库注入且被执行

sql注入

对于sql注入,我们可以通过预编译sql语句,来避免sql注入。

对于上面的注入示例代码,如果修改成这样就可以避免,这种方式是基于参数化查询(也称为预编译语句)来实现的,这是防止 SQL 注入的有效手段。

import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from './user.entity'; @Injectable() export class AppService { constructor( @InjectRepository(User) private readonly userRepository: Repository<User>, ) {} async findUserByUsernameSafe(username: string) { return await this.userRepository.findOne({ where: { username } }); } }

预编译sql语句,就是把sql语句分成两部分,一部分是sql语句本身,另一部分是sql语句的参数,参数不参与sql语句的编译,而是在sql语句编译之后,再对参数进行赋值。

编译好的sql语句:

SELECT * FROM user WHERE username = ?

数据库在执行时,会将这个参数 ' ; DROP TABLE user; -- 作为一个普通的字符串值填充到占位符的位置,而不会将其中的分号、单引号等字符解析为 SQL 语句的一部分

nosql注入
  • MongoDB: 类似sql注入防范,使用 MongoDB 官方驱动提供的安全查询方式,避免手动拼接查询条件。
  • Redis: 使用 Redis 客户端库提供的安全命令执行方法,避免将用户输入直接拼接到 Redis 命令中
数据库账号权限控制

不要直接使用root账户,为数据库用户分配最小的必要权限,只授予其完成业务所需的最低权限。例如,如果应用程序只需要读取数据,就不要给该用户赋予写入或删除数据的权限。

防范shell注入被执行

  • 避免拼接执行代码,如果一定要就要做检查过滤
  • 不要直接使用root账户,权限最小化

最后

总结一下,对于服务器注入这一类的攻击,只要做到以下几点,就可以避免注入被攻击:

  • 检查并过滤输入
  • 不到万不得已,避免拼接执行,如果一定要,执行之前要做好充分的检查过滤
最后编辑于

hi