Passer au contenu

Limitation de Taux

Une technique courante pour protéger les applications contre les attaques par force brute est la limitation de taux. Pour commencer, vous devez installer le package @nestjs/throttler.

Fenêtre de terminal
$ npm i --save @nestjs/throttler

Une fois l’installation terminée, le module ThrottlerModule peut être configuré comme n’importe quel autre package Nest avec les méthodes forRoot ou forRootAsync.

app.module.ts

app.module.ts
@Module({
imports: [
ThrottlerModule.forRoot([
{
ttl: 60000,
limit: 10,
}
]),
],
})
export class AppModule {}

Ce qui précède définira les options globales pour ttl, le temps de vie en millisecondes, et limit, le nombre maximum de requêtes dans la période de ttl, pour les routes de votre application qui sont protégées.

Une fois que le module a été importé, vous pouvez choisir comment vous souhaitez lier le ThrottlerGuard. Tout type de liaison mentionnée dans la section gardes est acceptable. Si vous souhaitez lier la garde globalement, par exemple, vous pouvez le faire en ajoutant ce fournisseur à n’importe quel module :

app.module.ts
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
}

Définitions de Limitation de Taux Multiples

Il peut arriver que vous souhaitiez configurer plusieurs définitions de limitation de taux, comme pas plus de 3 appels en une seconde, 20 appels en 10 secondes, et 100 appels en une minute. Pour ce faire, vous pouvez configurer vos définitions dans le tableau avec des options nommées, qui peuvent être référencées plus tard dans les décorateurs @SkipThrottle() et @Throttle() pour changer les options à nouveau.

app.module.ts

app.module.ts
@Module({
imports: [
ThrottlerModule.forRoot([
{
name: 'court',
ttl: 1000,
limit: 3,
},
{
name: 'moyen',
ttl: 10000,
limit: 20,
},
{
name: 'long',
ttl: 60000,
limit: 100,
},
]),
],
})
export class AppModule {}

Personnalisation

Il peut y avoir un moment où vous souhaitez lier le garde à un contrôleur ou globalement, mais vouloir désactiver la limitation de taux pour un ou plusieurs de vos points de terminaison. Pour cela, vous pouvez utiliser le décorateur @SkipThrottle(), pour annuler le limitation pour une classe entière ou une seule route. Le décorateur @SkipThrottle() peut également prendre un objet de clés de chaîne avec des valeurs booléennes s’il y a un cas où vous souhaitez exclure la plupart d’un contrôleur, mais pas chaque route, et le configurer par ensemble de limitations si vous en avez plus d’un. Si vous ne passez pas d’objet, la valeur par défaut est d’utiliser { default: true }.

users.controller.ts
@SkipThrottle()
@Controller('users')
export class UsersController {}

Ce décorateur @SkipThrottle() peut être utilisé pour ignorer une route ou une classe ou pour annuler le saut d’une route dans une classe qui est ignorée.

users.controller.ts
@SkipThrottle()
@Controller('users')
export class UsersController {
// La limitation de taux est appliquée à cette route.
@SkipThrottle({ default: false })
dontSkip() {
return 'Liste des utilisateurs avec limitation de taux.';
}
// Cette route ignorera la limitation de taux.
doSkip() {
return 'Liste des utilisateurs sans limitation de taux.';
}
}

Il existe également le décorateur @Throttle() qui peut être utilisé pour remplacer le limit et le ttl définis dans le module global, pour offrir des options de sécurité plus strictes ou plus lâches. Ce décorateur peut être utilisé sur une classe ou une fonction également. Avec la version 5 et ultérieure, le décorateur prend un objet avec la chaîne relative au nom de l’ensemble de limitation, et un objet avec les clés limit et ttl et des valeurs entières, similaire aux options passées au module racine. Si vous n’avez pas de nom défini dans vos options d’origine, utilisez la chaîne default. Vous devez le configurer de la manière suivante :

users.controller.ts
// Remplacer la configuration par défaut pour la limitation de taux et sa durée.
@Throttle({ default: { limit: 3, ttl: 60000 } })
@Get()
findAll() {
return "Liste des utilisateurs avec un taux de limitation personnalisé.";
}

Proxies

Si votre application fonctionne derrière un serveur proxy, vérifiez les options spécifiques de l’adaptateur HTTP (express et fastify) pour l’option trust proxy et activez-la. Cela vous permettra d’obtenir l’adresse IP d’origine dans l’en-tête X-Forwarded-For, et vous pouvez remplacer la méthode getTracker() pour extraire la valeur de l’en-tête plutôt que de req.ip. L’exemple suivant fonctionne avec express et fastify :

throttler-behind-proxy.guard.ts
import { ThrottlerGuard } from '@nestjs/throttler';
import { Injectable } from '@nestjs/common';
@Injectable()
export class ThrottlerBehindProxyGuard extends ThrottlerGuard {
protected async getTracker(req: Record<string, any>): Promise<string> {
return req.ips.length ? req.ips[0] : req.ip; // individualiser l'extraction IP selon vos propres besoins
}
}

Websockets

Ce module peut fonctionner avec des websockets, mais nécessite une certaine extension de classe. Vous pouvez étendre le ThrottlerGuard et remplacer la méthode handleRequest comme suit :

ws-throttler.guard.ts
@Injectable()
export class WsThrottlerGuard extends ThrottlerGuard {
async handleRequest(context: ExecutionContext, limit: number, ttl: number, throttler: ThrottlerOptions): Promise<boolean> {
const client = context.switchToWs().getClient();
const ip = client._socket.remoteAddress;
const key = this.generateKey(context, ip, throttler.name);
const { totalHits } = await this.storageService.increment(key, ttl);
if (totalHits > limit) {
throw new ThrottlerException();
}
return true;
}
}

Il y a quelques points à garder à l’esprit lors de l’utilisation de WebSockets :

  • Le garde ne peut pas être enregistré avec APP_GUARD ou app.useGlobalGuards()
  • Quand une limite est atteinte, Nest émettra un événement exception, donc assurez-vous qu’il y a un auditeur en place pour cela.

GraphQL

Le ThrottlerGuard peut également être utilisé pour travailler avec des requêtes GraphQL. Encore une fois, le garde peut être étendu, mais cette fois, la méthode getRequestResponse sera remplacée :

gql-throttler.guard.ts
@Injectable()
export class GqlThrottlerGuard extends ThrottlerGuard {
getRequestResponse(context: ExecutionContext) {
const gqlCtx = GqlExecutionContext.create(context);
const ctx = gqlCtx.getContext();
return { req: ctx.req, res: ctx.res };
}
}

Configuration

Les options suivantes sont valides pour l’objet passé au tableau des options du module ThrottlerModule :

nameLe nom pour le suivi interne de l’ensemble de limitation utilisé. Défaut à default si non passé
ttlLe nombre de millisecondes pendant lesquelles chaque requête sera conservée en mémoire
limitLe nombre maximum de requêtes dans la période de TTL
ignoreUserAgentsUn tableau d’expressions régulières des agents utilisateurs à ignorer lors de la limitation
skipIfUne fonction qui prend en entrée le ExecutionContext et renvoie un boolean pour court-circuiter la logique de limitation. Comme @SkipThrottler(), mais basé sur la requête

Si vous devez configurer un stockage à la place, ou voulez utiliser certaines des options ci-dessus de manière plus globale, s’appliquant à chaque ensemble de limitation, vous pouvez passer les options ci-dessus via la clé d’options throttlers et utiliser le tableau ci-dessous.

storageUn service de stockage personnalisé pour suivre la limitation. Voir ici.
ignoreUserAgentsUn tableau d’expressions régulières des agents utilisateurs à ignorer lors de la limitation
skipIfUne fonction qui prend en entrée le ExecutionContext et renvoie un boolean pour court-circuiter la logique de limitation. Comme @SkipThrottler(), mais basé sur la requête
throttlersUn tableau d’ensembles de limitation, définis en utilisant le tableau ci-dessus

Configuration Asynchrone

Vous souhaiterez peut-être obtenir votre configuration de limitation de taux de manière asynchrone au lieu de synchroniquement. Vous pouvez utiliser la méthode forRootAsync(), qui permet l’injection de dépendance et des méthodes async.

Une approche pourrait être d’utiliser une fonction de fabrication :

app.module.ts
@Module({
imports: [
ThrottlerModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => [
{
ttl: config.get('THROTTLE_TTL'),
limit: config.get('THROTTLE_LIMIT'),
},
],
}),
],
})
export class AppModule {}

Vous pouvez également utiliser la syntaxe useClass :

app.module.ts
@Module({
imports: [
ThrottlerModule.forRootAsync({
imports: [ConfigModule],
useClass: ThrottlerConfigService,
}),
],
})
export class AppModule {}

Ceci est réalisable tant que ThrottlerConfigService implémente l’interface ThrottlerOptionsFactory.

Stockages

Le stockage intégré est un cache en mémoire qui garde une trace des requêtes effectuées jusqu’à ce qu’elles aient dépassé le TTL défini par les options globales. Vous pouvez intégrer votre propre option de stockage à l’option storage du ThrottlerModule, tant que la classe implémente l’interface ThrottlerStorage.

Pour des serveurs distribués, vous pouvez utiliser le fournisseur de stockage de la communauté pour Redis pour avoir une source unique de vérité.

Helpers de Temps

Il existe quelques méthodes auxiliaires pour rendre les temps plus lisibles si vous préférez les utiliser plutôt que la définition directe. @nestjs/throttler exporte cinq helpers différents, seconds, minutes, hours, days, et weeks. Pour les utiliser, il suffit d’appeler seconds(5) ou l’un des autres helpers, et le bon nombre de millisecondes sera renvoyé.

Guide de Migration

Pour la plupart des gens, envelopper vos options dans un tableau suffira.

Si vous utilisez un stockage personnalisé, vous devez envelopper votre ttl et votre limit dans un tableau et l’assigner à la propriété throttlers de l’objet d’options.

Tout décorateur @ThrottleSkip() devrait maintenant prendre un objet avec des propriétés string: boolean. Les chaînes sont les noms des limitations. Si vous n’avez pas de nom, passez la chaîne default, car c’est ce qui sera utilisé sous le capot.

Tous les décorateurs @Throttle() devraient également maintenant prendre un objet avec des clés de chaîne, relatives aux noms des contextes de limitation (encore une fois, default s’il n’y a pas de nom) et des valeurs d’objets contenant les clés limit et ttl.

Pour plus d’infos, consultez le Changelog.