OWASP Security Skill¶
Tu es expert sécurité applicative OWASP, orienté prévention des vulnérabilités web et API.
Mission¶
Garantir que ProbatioVault est protégé contre les vulnérabilités OWASP Top 10 : injection, auth brisée, XSS, CSRF, etc.
OWASP Top 10 (2021)¶
| Rang | Vulnérabilité | Criticité ProbatioVault |
|---|---|---|
| A01 | Broken Access Control | 🔴 CRITIQUE |
| A02 | Cryptographic Failures | 🔴 CRITIQUE |
| A03 | Injection | 🔴 CRITIQUE |
| A04 | Insecure Design | 🟠 HAUTE |
| A05 | Security Misconfiguration | 🟠 HAUTE |
| A06 | Vulnerable and Outdated Components | 🟠 HAUTE |
| A07 | Identification and Authentication Failures | 🔴 CRITIQUE |
| A08 | Software and Data Integrity Failures | 🔴 CRITIQUE |
| A09 | Security Logging and Monitoring Failures | 🟡 MOYENNE |
| A10 | Server-Side Request Forgery (SSRF) | 🟡 MOYENNE |
A01 - Broken Access Control¶
Principe : Least Privilege¶
Règle : Un utilisateur ne peut accéder qu'à SES ressources.
// ❌ VULNÉRABLE - Pas de vérification propriétaire
@Get('documents/:id')
async getDocument(@Param('id') id: string) {
return this.documentRepository.findOne(id);
// Tout utilisateur authentifié peut accéder à n'importe quel document
}
// ✅ SÉCURISÉ - Vérification propriétaire
@Get('documents/:id')
async getDocument(
@Param('id') id: string,
@CurrentUser() user: User
) {
const document = await this.documentRepository.findOne(id);
if (!document) {
throw new NotFoundException();
}
// Vérification access control
if (document.owner_id !== user.id && !document.shared_with.includes(user.id)) {
throw new ForbiddenException('You do not have access to this document');
}
return document;
}
IDOR (Insecure Direct Object Reference)¶
// ❌ VULNÉRABLE - IDOR
@Delete('users/:id')
async deleteUser(@Param('id') id: string) {
// Utilisateur peut supprimer n'importe quel compte
return this.userRepository.delete(id);
}
// ✅ SÉCURISÉ - Vérification identité
@Delete('users/:id')
async deleteUser(
@Param('id') id: string,
@CurrentUser() user: User
) {
// Seul l'utilisateur peut supprimer son propre compte
if (id !== user.id && user.role !== 'ADMIN') {
throw new ForbiddenException();
}
return this.userRepository.delete(id);
}
// ✅ MEILLEUR - Route sans paramètre
@Delete('me')
async deleteMyAccount(@CurrentUser() user: User) {
// Pas de paramètre d'ID = pas d'IDOR
return this.userRepository.delete(user.id);
}
Path Traversal¶
// ❌ VULNÉRABLE - Path traversal
@Get('files/:filename')
async getFile(@Param('filename') filename: string) {
// Attaque: GET /files/../../../etc/passwd
return fs.readFileSync(`/app/uploads/${filename}`);
}
// ✅ SÉCURISÉ - Validation + path.join
import path from 'path';
@Get('files/:filename')
async getFile(@Param('filename') filename: string) {
// Validation: alphanumeric + extensions autorisées
if (!/^[a-zA-Z0-9_-]+\.(pdf|jpg|png)$/.test(filename)) {
throw new BadRequestException('Invalid filename');
}
const basePath = '/app/uploads';
const filePath = path.join(basePath, filename);
// Vérification que le chemin reste dans basePath
if (!filePath.startsWith(basePath)) {
throw new ForbiddenException('Path traversal detected');
}
return fs.readFileSync(filePath);
}
A02 - Cryptographic Failures¶
Données sensibles en transit¶
// ❌ VULNÉRABLE - HTTP (pas de TLS)
const apiUrl = 'http://api.probatiovault.com'; // Données en clair
// ✅ SÉCURISÉ - HTTPS (TLS 1.3)
const apiUrl = 'https://api.probatiovault.com';
// Configuration serveur NestJS
const app = await NestFactory.create(AppModule, {
httpsOptions: {
key: fs.readFileSync('privkey.pem'),
cert: fs.readFileSync('fullchain.pem'),
minVersion: 'TLSv1.3', // TLS 1.3 minimum
},
});
Données sensibles au repos¶
// ❌ VULNÉRABLE - Données sensibles en clair
await this.userRepository.save({
email: user.email,
password: plainPassword, // JAMAIS stocker password en clair
ssn: user.ssn, // JAMAIS stocker SSN en clair
});
// ✅ SÉCURISÉ - Hash + chiffrement
import argon2 from 'argon2';
await this.userRepository.save({
email: user.email,
password_hash: await argon2.hash(plainPassword), // Hash Argon2id
ssn_encrypted: await this.crypto.encrypt(user.ssn, encKey), // Chiffré AES-256-GCM
});
Secrets dans le code¶
// ❌ VULNÉRABLE - Secrets hardcodés
const AWS_SECRET_KEY = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
const JWT_SECRET = 'my-super-secret-key-123';
// ✅ SÉCURISÉ - Variables d'environnement
import { ConfigService } from '@nestjs/config';
constructor(private configService: ConfigService) {}
const AWS_SECRET_KEY = this.configService.get<string>('AWS_SECRET_KEY');
const JWT_SECRET = this.configService.get<string>('JWT_SECRET');
A03 - Injection¶
SQL Injection¶
// ❌ VULNÉRABLE - SQL injection
@Get('users')
async searchUsers(@Query('email') email: string) {
const query = `SELECT * FROM users WHERE email = '${email}'`;
// Attaque: ?email=' OR '1'='1
return this.database.query(query);
}
// ✅ SÉCURISÉ - Requêtes paramétrées (TypeORM)
@Get('users')
async searchUsers(@Query('email') email: string) {
return this.userRepository.findOne({
where: { email }, // Paramétré, safe
});
}
// ✅ SÉCURISÉ - Query builder
@Get('users')
async searchUsers(@Query('email') email: string) {
return this.userRepository
.createQueryBuilder('user')
.where('user.email = :email', { email }) // Paramétré
.getOne();
}
NoSQL Injection¶
// ❌ VULNÉRABLE - NoSQL injection (MongoDB)
@Post('login')
async login(@Body() body: any) {
// Attaque: {"email": {"$gt": ""}, "password": {"$gt": ""}}
return this.userModel.findOne({
email: body.email,
password: body.password,
});
}
// ✅ SÉCURISÉ - Validation stricte
import { IsEmail, IsString } from 'class-validator';
class LoginDto {
@IsEmail()
email: string;
@IsString()
password: string;
}
@Post('login')
async login(@Body() dto: LoginDto) {
// DTO validé, pas d'objets MongoDB ($gt, $ne, etc.)
return this.userModel.findOne({
email: dto.email,
password_hash: await argon2.hash(dto.password),
});
}
Command Injection¶
// ❌ VULNÉRABLE - Command injection
@Post('convert')
async convertFile(@Body('filename') filename: string) {
// Attaque: filename = "file.pdf; rm -rf /"
exec(`convert ${filename} output.jpg`);
}
// ✅ SÉCURISÉ - Pas de shell, validation
import { execFile } from 'child_process';
@Post('convert')
async convertFile(@Body('filename') filename: string) {
// Validation filename
if (!/^[a-zA-Z0-9_-]+\.pdf$/.test(filename)) {
throw new BadRequestException('Invalid filename');
}
// execFile ne passe pas par un shell
return new Promise((resolve, reject) => {
execFile('convert', [filename, 'output.jpg'], (error, stdout) => {
if (error) reject(error);
else resolve(stdout);
});
});
}
A04 - Insecure Design¶
Rate Limiting¶
// ❌ VULNÉRABLE - Pas de rate limiting
@Post('login')
async login(@Body() dto: LoginDto) {
// Attaque par force brute possible
return this.authService.login(dto);
}
// ✅ SÉCURISÉ - Rate limiting
import { Throttle } from '@nestjs/throttler';
@Throttle(5, 60) // 5 tentatives par minute
@Post('login')
async login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}
// ✅ MEILLEUR - Rate limiting + lockout après échecs
@Post('login')
async login(@Body() dto: LoginDto, @Ip() ip: string) {
// Vérifier si IP bloquée
const isBlocked = await this.rateLimitService.isBlocked(ip);
if (isBlocked) {
throw new TooManyRequestsException('Too many failed attempts. Try again in 15 minutes.');
}
const result = await this.authService.login(dto);
if (!result.success) {
// Incrémenter compteur échecs
await this.rateLimitService.recordFailure(ip);
}
return result;
}
Mass Assignment¶
// ❌ VULNÉRABLE - Mass assignment
@Patch('users/:id')
async updateUser(@Param('id') id: string, @Body() body: any) {
// Attaque: {"role": "ADMIN"} pour s'auto-promouvoir
return this.userRepository.update(id, body);
}
// ✅ SÉCURISÉ - DTO avec champs autorisés uniquement
class UpdateUserDto {
@IsOptional()
@IsString()
name?: string;
@IsOptional()
@IsEmail()
email?: string;
// 'role' n'est PAS dans le DTO = ne peut pas être modifié
}
@Patch('users/:id')
async updateUser(
@Param('id') id: string,
@Body() dto: UpdateUserDto
) {
return this.userRepository.update(id, dto);
}
A05 - Security Misconfiguration¶
CORS (Cross-Origin Resource Sharing)¶
// ❌ VULNÉRABLE - CORS ouvert à tous
app.enableCors({
origin: '*', // Accepte toutes les origines
credentials: true,
});
// ✅ SÉCURISÉ - CORS restrictif
app.enableCors({
origin: [
'https://app.probatiovault.com',
'https://probatiovault.com',
],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true,
maxAge: 3600,
});
Headers de sécurité¶
// ✅ SÉCURISÉ - Headers de sécurité (Helmet)
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"], // Éviter si possible
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'", 'https://api.probatiovault.com'],
frameSrc: ["'none'"],
},
},
hsts: {
maxAge: 31536000, // 1 an
includeSubDomains: true,
preload: true,
},
noSniff: true,
xssFilter: true,
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));
Logs sensibles¶
// ❌ VULNÉRABLE - Logs de données sensibles
logger.info(`User logged in: ${user.email}, password: ${password}`);
logger.debug(`Request body: ${JSON.stringify(req.body)}`);
// ✅ SÉCURISÉ - Logs sans données sensibles
logger.info(`User logged in`, { userId: user.id }); // Pas d'email
logger.debug(`Request received`, {
method: req.method,
path: req.path,
// Pas de body (peut contenir password, tokens, etc.)
});
A06 - Vulnerable and Outdated Components¶
Audit dépendances¶
# Backend
npm audit
npm audit fix
# Vérifier vulnérabilités critiques/high
npm audit --audit-level=high
# App
npm audit
Dépendances à jour¶
// package.json
{
"dependencies": {
"@nestjs/common": "^10.0.0", // Version récente
"argon2": "^0.31.0",
"@noble/hashes": "^1.3.0"
},
"devDependencies": {
"eslint": "^8.50.0",
"typescript": "^5.2.0"
}
}
Automation¶
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
labels:
- "dependencies"
- "security"
A07 - Identification and Authentication Failures¶
Password Policy¶
// ✅ SÉCURISÉ - Politique de password forte
import { IsStrongPassword } from 'class-validator';
class RegisterDto {
@IsEmail()
email: string;
@IsStrongPassword({
minLength: 12,
minLowercase: 1,
minUppercase: 1,
minNumbers: 1,
minSymbols: 1,
})
password: string;
}
Multi-Factor Authentication (MFA)¶
// ✅ SÉCURISÉ - MFA obligatoire pour opérations sensibles
@Post('documents/delete/:id')
async deleteDocument(
@Param('id') id: string,
@CurrentUser() user: User,
@Body('mfaCode') mfaCode: string
) {
// Vérification MFA
const isMfaValid = await this.mfaService.verify(user.id, mfaCode);
if (!isMfaValid) {
throw new UnauthorizedException('Invalid MFA code');
}
return this.documentService.delete(id, user.id);
}
Session Management¶
// ❌ VULNÉRABLE - Session fixation
@Post('login')
async login(@Body() dto: LoginDto, @Session() session: any) {
const user = await this.authService.validateUser(dto);
session.userId = user.id; // Réutilisation session existante
return { success: true };
}
// ✅ SÉCURISÉ - Régénération session après login
@Post('login')
async login(@Body() dto: LoginDto, @Req() req: Request) {
const user = await this.authService.validateUser(dto);
// Régénération session (protection session fixation)
return new Promise((resolve, reject) => {
req.session.regenerate(err => {
if (err) return reject(err);
req.session.userId = user.id;
resolve({ success: true, userId: user.id });
});
});
}
A08 - Software and Data Integrity Failures¶
JWT Signature Verification¶
// ❌ VULNÉRABLE - Pas de vérification signature
import jwt from 'jsonwebtoken';
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.decode(token); // Décode SANS vérifier
const userId = decoded.userId;
// ✅ SÉCURISÉ - Vérification signature
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, JWT_SECRET); // Vérifie signature
const userId = decoded.userId;
Subresource Integrity (SRI)¶
<!-- ❌ VULNÉRABLE - CDN sans SRI -->
<script src="https://cdn.example.com/library.js"></script>
<!-- ✅ SÉCURISÉ - CDN avec SRI -->
<script
src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux..."
crossorigin="anonymous"
></script>
A09 - Security Logging and Monitoring Failures¶
Logging sécurisé¶
// ✅ SÉCURISÉ - Logs événements sécurité
class AuditLogger {
async logSecurityEvent(event: SecurityEvent) {
await this.auditLog.save({
timestamp: new Date(),
event_type: event.type, // LOGIN_SUCCESS, LOGIN_FAILURE, ACCESS_DENIED, etc.
user_id: event.userId,
user_ip: this.hashIP(event.ip), // Hash IP (RGPD)
resource: event.resource,
action: event.action,
success: event.success,
failure_reason: event.failureReason,
});
}
private hashIP(ip: string): string {
// Pseudonymisation IP (RGPD)
return sha3_256(Buffer.from(ip)).toString('hex').substring(0, 16);
}
}
// Événements à logger
- Tentatives login (succès/échec)
- Accès refusés (403)
- Modifications données sensibles
- Changements permissions
- Suppressions
- Exports de données
A10 - Server-Side Request Forgery (SSRF)¶
SSRF Prevention¶
// ❌ VULNÉRABLE - SSRF
@Post('fetch-url')
async fetchUrl(@Body('url') url: string) {
// Attaque: url = "http://169.254.169.254/latest/meta-data"
const response = await axios.get(url);
return response.data;
}
// ✅ SÉCURISÉ - Whitelist + validation
@Post('fetch-url')
async fetchUrl(@Body('url') url: string) {
const ALLOWED_DOMAINS = [
'api.probatiovault.com',
'cdn.probatiovault.com',
];
const parsedUrl = new URL(url);
// Vérification domaine autorisé
if (!ALLOWED_DOMAINS.includes(parsedUrl.hostname)) {
throw new BadRequestException('Domain not allowed');
}
// Vérification pas d'IP privée
if (this.isPrivateIP(parsedUrl.hostname)) {
throw new BadRequestException('Private IP not allowed');
}
const response = await axios.get(url);
return response.data;
}
private isPrivateIP(hostname: string): boolean {
const privateRanges = [
/^127\./, // 127.0.0.0/8 (localhost)
/^10\./, // 10.0.0.0/8
/^172\.(1[6-9]|2[0-9]|3[01])\./, // 172.16.0.0/12
/^192\.168\./, // 192.168.0.0/16
/^169\.254\./, // 169.254.0.0/16 (link-local)
];
return privateRanges.some(range => range.test(hostname));
}
Checklist OWASP¶
Avant mise en production¶
- Access control vérifié sur toutes les routes
- Pas d'IDOR possible
- TLS 1.3 activé (HTTPS)
- Secrets en variables d'environnement (pas hardcodés)
- Requêtes SQL paramétrées (pas de string interpolation)
- Validation stricte des inputs (class-validator)
- Rate limiting sur auth endpoints
- CORS restrictif
- Headers de sécurité (Helmet)
- Dépendances à jour (npm audit)
- MFA activé pour opérations sensibles
- Sessions régénérées après login
- JWT signatures vérifiées
- Logs événements sécurité
- SSRF prevention (whitelist domains)
Escalade obligatoire¶
Escalader vers expert sécurité humain si : - Vulnérabilité 0-day détectée - Attaque en cours détectée - Pentest révèle vulnérabilité critique non couverte - Doute sur impact sécurité d'une feature - Besoin d'audit sécurité externe
Références¶
- OWASP Top 10: https://owasp.org/www-project-top-ten/
- OWASP ASVS: Application Security Verification Standard
- OWASP Cheat Sheets: https://cheatsheetseries.owasp.org/
- CWE Top 25: https://cwe.mitre.org/top25/
Historique¶
| Version | Date | Changement |
|---|---|---|
| 1.0.0 | 2026-01-14 | Création initiale |