NestJS小技巧22-最大限度地提高NestJS应用程序中的代码安全性(part2)
by 雪隐 from https://juejin.cn/user/1433418895994094
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权
在第一部份,我讲了4种常见的安全风险以及在NestJS中规避他们的方法,值得注意的是,尽管这里使用了NestJS(作为我最喜欢的API框架之一),但这些最佳实践与框架无关。
让我们继续深入了解其他常见的安全漏洞。
- 注入 Injection
- 缺乏资源和费率限制 Lack of resources and rate limiting
- 身份验证和身份验证失败 Identification and Authentification Failure
- 缺少对象级访问控制 Missing Object level access control
注入
作为最著名的漏洞之一,当攻击者通过将任意代码或命令注入应用程序来执行这些代码或命令时,就会发生注入。注入攻击有很多形式,比如SQL注入,命令行注入,表达式注入。
虽然注入漏洞非常有名,但是它仍然经常发生。最近发生的一些事件包括2017年影响1.47亿用户的Equifax数据泄露事件、2018年泄露38万信用卡详细信息的英国航空公司数据泄露事件,以及2019年因SQL注入而暴露1亿用户个人信息的Capital One泄露事件。
以下是一个SQL注入的例子
import { Injectable } from '@nestjs/common';
import { Connection } from 'typeorm';
@Injectable()
export class ClientService {
constructor(private connection: Connection) {}
async getClients(name: string) {
const query = `SELECT * FROM client WHERE name = '${name}'`;
return await this.connection.query(query);
}
}
// Client Controller
@Controller('users')
export class ClientController {
constructor(private clientService: ClientService) {}
@Get('search')
async searchClient(name: string) {
return this.clientService.getClients(name);
}
}
在这个例子中,ClientService
类使用name
参数来构造SQL的query从而运行数据库命令。如果name
这个参数不干净,攻击者可能注入带有特殊字符且怀有恶意的代码。比如,攻击者可以给name
注入参数类似'; DROP TABLE client; --
,这个命令会删除client
表。
SQL注入可以通过清除用户输入并尽可能使用准备好的语句或参数化查询来防止。这里针对上一个例子做出了一点改善。
import { Injectable } from '@nestjs/common';
import { Connection } from 'typeorm';
@Injectable()
export class ClientService {
constructor(private connection: Connection) {}
async getClients(name: string) {
const query = 'SELECT * FROM client WHERE name = $1';
const params = [name];
return await this.connection.query(query, params);
}
}
修订后的ClientService
使用参数化查询,这有助于确保用户输入被视为数据而非可执行代码。
另一种常见的注入攻击类型是操作系统命令注入。当攻击者注入任意操作系统命令并执行该命令时,就会发生这种情况;注入可以通过请求头或参数等来完成。这里是一个例子。
@Injectable()
export class ClientService {
public executeCommand(command: string): void {
const commandArray = command.split(' ');
spawn(commandArray[0], commandArray.slice(1));
}
}
如果没有对命令输入进行适当的清理或验证,上面的代码就会为“rm-rf/var/www”
这样的恶意命令打开大门。
为了防止操作系统命令注入,最好的方法是用框架特殊API来替代操作系统命令。另外,我们应该转义并验证用户输入,以确保只有预期的输入才能通过验证。
缺乏资源和费率限制
许多客户端可以同一时间调用API。如果大量的同时请求超过了限制,API会变得没有响应甚至崩溃。
此漏洞可能由高峰时段合法请求的突然激增或恶意DDoS攻击触发。最近的DDoS攻击之一是2020年AWS网络服务攻击。
当攻击者在很短的时间内发送大量的请求,任何端点如果没有速率限制会变得很脆弱。为了防止它,我们可以在NestJS中使用速率限制的中间件。比如nestjs/throttler
或者express-rate-limit
在下面的例子中,我使用nestjs/throttler
来限制从同一个IP地址1分钟之内最大只能请求10次这个端口。它适用于应用程序的所有传入请求。
@Module({
imports: [
ThrottlerModule.forRoot({
ttl: 60,
limit: 10,
}),
],
})
export class AppModule {}
它还有别的设定参数来自定义节流阀。例如,您可以使用@SkipThrottle
装饰器来使一个端点速率限制无效,或者使用@Throttle()
装饰器去覆盖全局的limit
和ttl
的设定。
除了请求数量之外,还应考虑以下限制。
- 执行超时:如果请求需要很长时间才能完成,则应终止请求。
- 有效负载大小/如果请求返回潜在的大量数据,则响应的最大数据数
- 最大可分配内存:过度使用内存可能导致应用程序崩溃。
身份验证和身份验证失败
身份验证和身份验证失败是与应用程序的身份验证和识别过程相关的漏洞。
当系统或应用程序没有可靠的方法来验证用户身份时,或者当身份验证过程很容易被绕过或操纵时,就会发生这种情况。
这个漏洞可以通过不同形式。最通常的方式是session劫持。这里有一个例子来说明它是怎么在NestJS应用中发生的。
- 攻击者截获普通用户的会话cookie。这些cookie可能包含用户的ID或角色等信息以及其他识别信息。
// Cookies content
eyJ1c2VySWQiOjEyMzQ1LCJ1c2VyUm9sZSI6Im5vcm1hbCJ9
// JSON
{"userId":12345,"userRole":"normal"}
- 攻击者修改这个会话cookie改变用户的角色和别的认证信息来匹配管理员的账号权限。
- 攻击者将修改后的cookies返回给服务端,假装他是管理员。服务端接收到被修改后的cookies并准许攻击者利用管理者访问权限来访问接口。
上面的例子显然是由服务器上的身份验证不足引起的。在NestJS中防止这种类型攻击的一种方法是使用JWT(JSONWeb令牌)实现身份验证。NestJS提供了一个用于jwt操作的@NestJS/jwt包。您可以在此处找到有关在NestJS应用程序中实现JWT的更多详细信息。
您可以考虑实现MFA(multi-factor authentication)来进一步加强身份验证。最流行的MFA形式是OTP(one-time passcode)
您可以创建您自己的OTP服务来存储一次性密码并且通过电话和邮箱来管理发送。如果你不想重新发明轮子,我们可以使用像otplib这样的现有库。
以下是在NestJS中使用otplib的基本步骤
// install Otplib
npm install otplib
// import it
import * as OTPLib from 'otplib';
// generate a secrete
const secret = OTPLib.authenticator.generateSecret();
// then, we can gerenate a QRCode url to show a QRCode in your app
// we should save the generated secret in database for later use
const otpUrl = OTPLib.authenticator.keyuri('user', 'The App name', secret);
// Now user scan the QRCode and sent it to the NestJS endpoint
// we can use the built in authenticator.verify to validate the otp token
import { authenticator } from 'otplib';
authenticator.verify({
token: token // Sent from Client,
secret: secrete // previously saved secrete
})
如果MFA不能被实现,以下是别的可以考虑的选择
- 安全问题
- 验证码
- 需要强密码
缺少对象级访问控制
对象级访问控制是一种安全机制,可以根据请求访问的用户的权限或角色来控制对特定对象或资源的访问。
下面是NestJS应用程序中可能出现的风险示例:
import { Injectable } from '@nestjs/common';
import { ClientService } from './client.service';
@Injectable()
export class AttachmentController {
constructor(private clientService: ClientService) {}
@Get('/document/:id')
public async getFile(id: string): Promise<any> {
return await this.clientService.getAttachmentById(id);
}
}
在本例中,AttachmentController
中的getFile
方法没有对象级访问控制,无法确保只有附件所有者或具有权限的用户才能访问它。攻击者可能会通过猜测附件ID来访问系统中的任何文件。
为了防止风险,我们可以验证用户是该对象或资源的所有者或具有访问该对象或源的权限。
@Injectable()
export class AttachmentController {
constructor(private clientService: ClientService) {}
@Get('/document/:id')
public async getFile(id: string): Promise<any> {
const currentUser = getCurrentUser();
const document = await this.clientService.getAttachmentById(id);
if (currentUser.id !== document.ownerId) {
throw new ForbiddenException();
}
return document;
}
}
请注意,以上是一个基于只有附件文件的所有者才能访问它的假设的人为示例。
为了进一步降低风险,我们还可以使用难以预测的随机值作为记录Id。这有助于防止攻击者猜测或枚举记录ID。下面是一个使用uuid模块为文档id生成随机唯一值的示例。
import { v4 as uuid } from 'uuid';
const document = {
id: uuid(),
ownerId: currentUser.id,
最佳实践是确保服务器端功能在NestJS应用程序中受到基于角色的授权保护守卫。
最后的想法
人们普遍认为,安全是软件开发周期的最后一步,无论是以渗透测试的形式还是通过使用静态扫描工具。然而,这种方法是不够的。
相反,安全性应该集成到从设计到编码和测试的每一个开发过程步骤中。安全考虑应该是规划和开发软件应用程序的关键部分,而不是事后考虑。
转载自:https://juejin.cn/post/7250080519069614141