NestJS小技巧13-不要暴露您的NestJS API

3,735 阅读6分钟
by 雪隐 from https://juejin.cn/user/1433418895994094
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权

原文链接

API应该是安全的,以防止潜在的攻击。然而,令人惊讶的是,简单的人为错误往往会导致API端点不受保护。最近的一个例子是Optus数据泄露,它是由“访问客户数据不需要身份验证的API”引起的。这突出了对API进行彻底测试和严格安全措施以防止此类事件发生的重要性。

未受保护的API是OWASP的主要漏洞之一。其定义如下:

现代应用程序通常涉及富客户端应用程序和API,例如浏览器中的JavaScript和移动应用程序,
它们连接到某种API。这些API通常不受保护,并且包含许多漏洞。

为了避免未受保护的API,第一道防线是通过单元测试。在本文中,我们将讨论如何使用单元测试来确保NestJS控制器和端点得到保护。


守卫Guards

在NestJS中,我们使用身份验证/授权保护来保护控制器或端点。使用Guards,我们可以确保只有授权用户才能访问API。以下是身份验证保护的示例。

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return this.validateRequest(request);
  }

  private validateRequest(request: any) {
    // 对用户请求进行身份验证以确保其经过身份验证
    // 即验证令牌
  }
}

AuthGuard类包含canActivate方法,该方法将ExecutionContext对象作为参数。该方法执行验证以确定是否应允许该请求。

要将AuthGuard应用于控制器,我们可以使用@UseGuards装饰器

@UseGuards(AuthGuard)  
@Controller()  
export class AppController {}

如果@UseGuards装饰器被意外删除,API将变得不受保护,并容易受到未经授权的访问。为了确保Guard正确应用于控制器,我们可以使用单元测试来验证其存在。

为了实现这一点,我们需要使用反射元数据库和NestJS未记录的DiscoveryService API。让我们先来看反射元数据库。

Reflect-MetaData

反射元数据库提供对反射API的支持,反射API是ECMAScript规范的一部分。我们可以在运行时使用它来获取元数据。值得注意的是,NestJS还在后台使用反射元数据来处理元数据。

下面的示例演示了如何从AppController中检索Guards元数据。

const guards = Reflect.getMetadata('__guards__', AppController);

getMetaData方法采用两个参数:

  • metadataKey:用于存储和检索元数据的密钥。在这种情况下,密钥是__guards__,NestJs使用它来引用guards。

  • target:在其上定义元数据的目标对象。

使用getMetadata方法,我们可以编写一个简单的单元测试来验证AppController是否受到保护。

it('should AuthGard be applied to the AppController', async () => {  
  const guards = Reflect.getMetadata('__guards__', AppController);  
  const guard = new guards[0]();  
  expect(guard).toBeInstanceOf(AuthGuard);  
});

如果应用程序中有多个控制器,是否可以同时编写一个涵盖所有控制器的单元测试?答案是肯定的,但我们需要使用NestJS发现服务。


发现服务

NestJS发现服务是一个未记录的公共API。需要注意的是,“未记录”的功能在未来可能会发生更改或损坏。虽然这是一个方便使用的功能,但通常最好避免依赖应用程序中未记录的功能。在我个人看来,只要你意识到风险,在单元测试中使用它是可以接受的。

如下所示,使用this.discoveryService.getControllers(),我们可以获得InstanceWrapper={metatype,name,instance,…}类型的集合。

const controllers = await discoveryService.getControllers({});

要从InstanceWrapper中提取防护元数据,我们可以使用getEnhancersMetadata方法。在下面的测试中,我们遍历每个控制器,并验证它们是否受到AuthGuard的保护。

it("should have AuthGard applied for all controllers", async () => {
  const controllers = await discoveryService.getControllers({});
  controllers.map((c) => {
    const guard = c
      .getEnhancersMetadata()
      ?.filter(
        ({ instance }: InstanceWrapper) => instance instanceof AuthGuard
      );
    expect(guard[0].name).toEqual("AuthGuard");
  });
});

为了实现精细级别的访问控制,我们可以定义RoleGuard并将其应用于各个端点。我们在下面的函数中使用SetMetadata来分配具有特定键的元数据。SetMetadata是一个开箱即用的NestJS装饰器函数。

import { SetMetadata } from '@nestjs/common';  
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

要将角色装饰器应用于端点,我们需要将角色传递给装饰器

@Get()  
@Roles('Admin')  
getAll() {  
  return [];  
}

保护端点

为了检测端点是否与角色装饰器相关联,我们使用如下所示的getMetadata

const decorators = Reflect.getMetadata(  
  'roles',  
  DevController.prototype.getAll,  
);

在单元测试中,我们可以验证端点是否受到具有角色的角色装饰器的保护

it("should getAll be accessible by Admin role only", () => {
  const decorators = Reflect.getMetadata(
    "roles",
    DevController.prototype.getAll
  );
  expect(decorators).toContain("Admin");
});

Reflect API可用于获取端点的其他元数据。

it("should getHello has correct path and http method", () => {
  const path = Reflect.getMetadata("path", appController.getHello);
  expect(path).toBe("/");
  const method = Reflect.getMetadata("method", appController.getHello);
  expect(method).toBe(RequestMethod.GET);
});

在上面对getMetaData的使用中,使用的键是“path”和“method”,它们分别对应于getHello方法的path和HTTP方法。


奖励内容:使用发现服务动态获取服务列表

发现服务可以很好地解决某些问题。例如,在我最近的一个NestJS项目中,有一个MapperResolver类:

@Injectable()
export class MapperResolver {
  private mapperList: IMapper[];
  
  constructor(
    private serviceAMapper: ServiceAMapper,
    private serviceBMapper: ServiceBMapper,
    private serviceCMapper: ServiceCMapper,
    private serviceDMapper: ServiceDMapper,
  ) {
    this.mapperList = [
      serviceAMapper,
      serviceBMapper,
      serviceCMapper,
      serviceDMapper
    ];
  }
  public Resolve(serviceType: string): IMapper {
    const mapper = this.mapperList.find(c => c.serviceType === serviceType);
    if (mapper) {
      return mapper;
    }
    throw new Error(`No Mapper found`);
  }
}

在现实世界的项目中,有10多个Mapper类被注入到MapperResolver类构造函数中,随着新功能的添加,这个数字还在继续增长。这已成为一个维护问题。

我们可以使用发现服务来解决这个问题。由于此主题不在本文的范围内,我将只对解决方案进行简要描述。

  • 创建一个接受参数的decorator@ServiceRegister

  • 将装饰器添加到每个Mapper类,即@ServiceRegister(“Mapper”)

  • 使用discoveryService.getProviders()检索所有提供程序,并使用元数据筛选出映射器服务。

最终的结果是,我们能够删除MapperResolver类中所有注入的Mapper服务。但是,请再次注意使用DiscoveryService,因为它是一个未经文档记录的API。


总结

定期测试和监控应用程序的安全性以确保其安全可靠是很重要的。通过将安全检查嵌入到单元测试中,并定期运行这些测试,我们可以发现任何问题并在它们在生产中引起问题之前进行修复。

祝大家快乐的编程!