Sécurisez (insuffisamment mais c'est mieux que rien) votre application Nest pour avoir bonne conscience
Pour beaucoup, c'est toujours excessivement pénible de devoir sécuriser une application qui devrait simplement fonctionner (coucou les utilisateurs de Bolt !).
Mais comme on ne peut pas faire confiance aux utilisateurs (ni à nous-mêmes d'ailleurs), autant voir les bases ensemble, quitte à ce que ça ne soit pas suffisant
Spoiler : effectivement, ça n'est pas suffisant, mais au moins c'est un début
1. L'authenfititi... l'authenticafi... l'authentification, le B.A. BA
Commençons par un classique : l'authentification.
bcrypt, c'est la base, mais encore faut-il bien l'implémenter. Prenons un exemple concret :
import * as bcrypt from 'bcrypt';
const saltOrRounds = 10;
const password = 'random_password';
const hash = await bcrypt.hash(password, saltOrRounds);
On prend le password, on le crypte et on a plus qu'à enregistrer ça en base de données. Cela va sans dire, mais c'est mieux en le disant.
@Injectable()
export class AuthService {
async validateUser(email: string, password: string): Promise<any> {
const user = await this.usersService.findOne(email);
// Le code sous entend que vous avez préalablement crypté le password avec bcrypt ou bcryptjs
if (!user || !bcrypt.compare(password, user.password)) {
throw new UnauthorizedException('USURPATEUR !');
}
// Ne JAMAIS retourner le mot de passe, même hashé
const { password: _, ...result } = user;
return result;
}
}
Ici, on compare l'input password à ce que l'on a en base, crypté avec bcrypt. On ne décrypte jamais le mot de passe en base, on crypte l'input pour le comparer à ce qu'on a stocké.
Ca me rappelle une startup qui stockait les mots de passe en base64 : 100 000 comptes compromis en 2 heures (On saluera quand même l'effort d'avoir vouloir faire quelque chose avec les mots de passe)
2. UseGuards() & Roles()
Nest permet d'injecter des Guards entre la requête HTTP de vos utilisateurs, et la route qui va être executée.
Un guard correspond à un contrôle des droits d'un utilisateur (son appartenance à un groupe, à une typologie d'utilisateurs, au fait qu'il dispose d'une session valide... bref, c'est flexible).
Ils peuvent être globaux (pour toute l'application), globaux au niveau d'un controller (et pour toutes ses routes), ou propres à une route en particulier.
Associé aux Roles(), ca vous permet de définir les rôles qui peuvent accéder à une ressource.
Comme c'est déjà excessivement bien documenté dans le fabuleuse documentation de Nest, autant vous inviter à y aller directement : https://docs.nestjs.com/guards
3. Sessions
Les sessions, ça doit rester ce que c'est censé être : temporaire.
Un exemple avec Redis avec une durée de vie limitée et une rotation des tokens.
@Injectable()
export class SessionService {
constructor(private readonly redisService: RedisService) {}
async createSession(userId: string): Promise<string> {
const token = crypto.randomBytes(32).toString('hex');
// 4 heures max, rotation obligatoire, si t'es pas content, ENVOI MOI UN FAX
await this.redisService.set(`session:${token}`, userId, 'EX', 14400);
return token;
}
}
4. XSS, CSRF,Injection SQL... c'est un monde dangereux
Nettoyez toujours au moins à l'aide de vos DTOs...
Avec class-validator et class-transformer, ça se fait les doigts dans le nez. Vous pouvez valider les données avec des règles qui dépassent l'imagination (ou alors, le plus souvent, avec IsNotEmpty et IsEmail)
// create-article.dto.ts
import { IsString, IsNotEmpty } from 'class-validator';
export class CreateArticleDto {
@IsString()
@IsNotEmpty()
label: string;
@IsString()
@IsNotEmpty()
description: string;
// Ajouter ce que vous voulez qui correspond à votre model, avec les règles que vous voulez
}
Et si vous êtes un peu foufou, vous pouvez aussi utiliser DOMPurify côté serveur pour nettoyer ce que vous pourriez avoir en input
@Controller('articles')
export class ArticlesController {
@Post()
create(@Body() articleDto: CreateArticleDto) {
// DOMPurify côté serveur, parce qu'on n'est jamais trop prudent
articleDto.content = DOMPurify.sanitize(articleDto.content, {
ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong'],
ALLOWED_ATTR: []
});
return this.articlesService.create(articleDto);
}
}
... rajoutez en une couche avec doubleCsrf...
Parce qu'un middleware global qui gère les commandes non autorisée, ca ne fait jamais de mal
// https://docs.nestjs.com/security/csrf <- la doc officielle de Nest
import { doubleCsrf } from 'csrf-csrf';
// ...
// somewhere in your initialization file
const {
invalidCsrfTokenError, // This is provided purely for convenience if you plan on creating your own middleware.
generateToken, // Use this in your routes to generate and provide a CSRF hash, along with a token cookie and token.
validateRequest, // Also a convenience if you plan on making your own middleware.
doubleCsrfProtection, // This is the default CSRF protection middleware.
} = doubleCsrf(doubleCsrfOptions);
app.use(doubleCsrfProtection);
Arretez de vivre en 2001 avec des requêtes SQL à la papa
On utilisera un ORM pour limiter les injections SQL, en dehors du fait qu'un ORM, c'est beaucoup plus que ça, on est bien d'accord.
Prisma, TypeORM... ce que vous voulez tant que ça vous évite de faire une requête du genre :
const query = `SELECT * FROM users WHERE email = '${email}'`; // Brrr, ca me file des frissons
Uploads & Downloads, & stockage
Déportez tout ce qui est lié à l'upload de fichiers. Par exemple, avec AWS, vous avez les buckets S3 avec les urls présignées.
let params = {
Bucket: bucketName,
Key: key,
ContentType: contentType,
Expires: 1800
};
const signedUrl = await s3.getSignedUrlPromise('putObject', params);
Vous prenez en entrée les infos du File côté client, vous préparez une url signée temporaire avec vos paramètres aux petits oignons.
Cerise sur le chapeau, l'upload se fait côté client directement sur le bucket.
Et bien sûr vous pouvez controler le content type et le poids maximum du fichier (content-length-range).
Vous pouvez ensuite accorder un accès temporaire aux fichiers, toujours à partir d'une url signée générée côté back, tout en bénéficiant des stratégies que vous définissez sur le bucket en lui même.
Je parle de S3 parce que c'est mon dada, mais chez les autres vous avez :
- Google Cloud Storage Signed URLs
- Azure SAS (Shared Access Signatures) Tokens
- OVH : Object Storage
Le principe est le même à chaque fois avec les particularités de chaque plateforme (parce que l'homogénéisation, déjà c'est difficile à écrire, alors en plus l'avoir sur toutes les plateformes...)
Headers : Port du casque obligatoire
Pour renforcer la sécurité de votre application, rien de tel qu'un Helmet bien configuré qui va se charger de définir vos headers.
C'est facile à mettre en place, et donc INDISPENSABLE.
import helmet from 'helmet';
app.use(helmet());
Vous pouvez bien entendu configurer Helmet avec vos propres besoins.
Et par pitié, ne négligez pas l'importance des CORS.
Log.
Même si vous n'y voyez pas d'intérêt immédiat, ne vous posez pas de question : loggez tout. Et n'importe quoi.
La journalisation (qu'on a déjà vu dans mon article sur CQRS), c'est l'art de tout consigner pour remonter efficacement dans le passé et voir à quel moment ça a merdé, et pourquoi.
Parce que malgré les efforts fournis par toutes les entreprises du monde pour renforcer la sécurité de leurs applications, la triste vérité est que, si on veut vraiment vous hacker, il y a de fortes chances pour que ça finisse par arriver un jour. C'est comme ça. C'est la vie. Deal with it.
Mettez en place un Logger, à minima comme celui ci :
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { ip, method, path: url } = request;
return next.handle().pipe(
tap({
next: (res) => {
Logger.log(`${method} ${url} ${ip} - Success`);
},
error: (error) => {
Logger.error(`${method} ${url} ${ip} - ${error.message}`);
},
}),
);
}
}
Dans l'idéal, confiez ça à un service qui connait bien son affaire, par exemple https://www.datadoghq.com/ (OP non sponsorisée hein)
NPM with love (et un peu de haine aussi)
C'est un fait, avec NPM (ou yarn, ou pnpm... bref vous avez pigé), on peut rapidement se retrouver avec des dépendances obsolètes qui présentent des failles de sécurité à terme, voir des dépendances carrément malveillantes.
Pensez à auditer vos dépendances, à minima avec npm audit, mais dans l'idéal avec un truc carrément plus robuste comme snyk
# Pas foufou, mais c'est un bon debut
npm audit
# Beacoup mieux
snyk test
# Masterclass : intégrer ça directement dans votre CI
C'est pas fini
Alors, pour cet article en lui même, si, c'est fini.
Par contre la quête vers une application sécurisée est eternelle (autant que les vidéos de builds Elden Ring de Playmoo).
- Gardez un oeil sur les derniers articles relatifs à la cybersécurité, par exemple en vous abonnant à la mailing TLDR.
- Allez faire un tour sur le site de l'OWASP régulièrement
- Parlez en autour de vous
- Sensibilisez tout le monde dans l'entreprise (pas seulement votre squad)
- Serrez les fesses et croisez les doigts (ou l'inverse selon votre souplesse)
Et priez. Pauvres mortels.