Aller au contenu

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