La sécurité est la priorité absolue pour OFM. Ce document décrit toutes les mesures de sécurité implémentées et les bonnes pratiques à suivre.
Configuration:
- Algorithm: HS256
- Access Token: 15 minutes d'expiration
- Refresh Token: 7 jours d'expiration
- Secret rotation: Tous les 90 jours (recommandé)
Implémentation:
// Access token payload
{
sub: userId,
email: user.email,
username: user.username,
role: user.role,
iat: issuedAt,
exp: expiresAt
}Sécurité:
- ✅ Secrets stockés dans variables d'environnement
- ✅ Jamais exposés côté client
- ✅ Rotation automatique des refresh tokens
- ✅ Blacklist des tokens révoqués (Redis)
Méthode: TOTP (Time-based One-Time Password)
- Compatible Google Authenticator, Authy, etc.
- Window de 2 périodes (60 secondes)
- QR code généré pour setup facile
Flow d'activation:
1. User: POST /auth/2fa/enable
2. Server: Génère secret + QR code
3. Server: Stocke secret temporairement (Redis, 10min)
4. User: Scanne QR avec app authenticator
5. User: POST /auth/2fa/verify { code: "123456" }
6. Server: Vérifie code
7. Server: Active 2FA + sauvegarde secret (DB encrypted)
Algorithme: bcrypt
Rounds: 12 (configurable via BCRYPT_ROUNDS)
// Never do this
password === hashedPassword ❌
// Always do this
await bcrypt.compare(password, hashedPassword) ✅Politique de mots de passe:
- Minimum 8 caractères
- Au moins 1 majuscule
- Au moins 1 minuscule
- Au moins 1 chiffre
- Au moins 1 caractère spécial (@$!%*?&)
Rôles:
enum UserRole {
CREATOR, // Créateur de contenu
SUBSCRIBER, // Abonné
ADMIN, // Administrateur
MODERATOR // Modérateur
}Implémentation:
@Get('stats')
@Roles(UserRole.CREATOR)
@UseGuards(JwtAuthGuard, RolesGuard)
async getStats(@CurrentUser() user) {
// Seuls les créateurs peuvent accéder
}Vérification automatique que l'utilisateur est propriétaire de la ressource :
// ❌ Mauvais - N'importe qui peut modifier
@Put('content/:id')
async update(@Param('id') id: string, @Body() dto) {
return this.contentService.update(id, dto);
}
// ✅ Bon - Vérification du propriétaire
@Put('content/:id')
async update(
@Param('id') id: string,
@CurrentUser('id') userId: string,
@Body() dto
) {
const content = await this.contentService.findOne(id);
if (content.creatorId !== userId) {
throw new ForbiddenException();
}
return this.contentService.update(id, dto);
}Base de données:
- PostgreSQL: Encryption native avec pgcrypto
- Champs sensibles:
twoFactorSecret, données bancaires - Méthode: AES-256-GCM
-- Exemple de champ chiffré
CREATE TABLE users (
id UUID PRIMARY KEY,
two_factor_secret BYTEA -- Encrypted with pgcrypto
);Fichiers médias:
- S3 Server-Side Encryption (SSE-S3)
- KMS pour clés de chiffrement
- Access via signed URLs uniquement
TLS/SSL:
- TLS 1.3 minimum
- HSTS activé (max-age: 31536000)
- Certificats Let's Encrypt (auto-renouvelés)
Configuration Nginx:
server {
listen 443 ssl http2;
ssl_protocols TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
}Masking dans les logs:
// ❌ Ne jamais logger
logger.log('User login', { password: user.password });
// ✅ Toujours masquer
logger.log('User login', {
userId: user.id,
email: maskEmail(user.email) // u***@example.com
});RGPD Compliance:
- Droit à l'oubli: Soft delete + anonymisation après 30j
- Portabilité: Export JSON des données
- Consentement: Tracking explicite
Global:
- 100 requêtes / minute par IP
- 1000 requêtes / heure par utilisateur
Endpoints sensibles:
@Post('login')
@Throttle(5, 60) // 5 tentatives / minute
async login(@Body() dto: LoginDto) {
// ...
}
@Post('register')
@Throttle(3, 3600) // 3 comptes / heure par IP
async register(@Body() dto: RegisterDto) {
// ...
}Stockage: Redis avec clés ratelimit:{ip}:{endpoint}
Login:
- Max 5 tentatives / 15 minutes
- Lockout 1h après 5 échecs
- Captcha après 3 échecs
2FA:
- Max 3 tentatives / 5 minutes
- Lockout temporaire après échecs
Configuration:
app.enableCors({
origin: ['https://app.ofm.com', 'https://creators.ofm.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['X-Total-Count', 'X-Page'],
});origin: '*' // ❌ DANGER !Helmet Configuration:
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'", 'https://api.stripe.com'],
frameSrc: ["'none'"],
objectSrc: ["'none'"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
noSniff: true,
xssFilter: true,
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
})class-validator sur tous les DTOs :
export class RegisterDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/)
password: string;
}✅ Prisma ORM - Protection automatique
// ✅ Safe - Parameterized query
await prisma.user.findUnique({
where: { email: userInput }
});
// ❌ NEVER use raw SQL with user input
await prisma.$executeRaw(`SELECT * FROM users WHERE email = '${userInput}'`);Sanitization:
- Tous les inputs HTML sont échappés
class-sanitizerpour nettoyage automatique- CSP headers pour bloquer scripts inline
import { sanitize } from 'class-sanitizer';
@Post('comment')
async create(@Body() dto: CreateCommentDto) {
dto.text = sanitize(dto.text); // Remove HTML tags
return this.commentsService.create(dto);
}Type MIME vérification:
const ALLOWED_IMAGES = ['image/jpeg', 'image/png', 'image/webp'];
const ALLOWED_VIDEOS = ['video/mp4', 'video/quicktime'];
if (!ALLOWED_IMAGES.includes(file.mimetype)) {
throw new BadRequestException('Invalid file type');
}Taille maximale:
- Images: 10 MB
- Vidéos: 500 MB
- Audio: 50 MB
Scanning:
- ClamAV pour scan antivirus
- EXIF stripping pour remove metadata
- Content-Type sniffing prevention
Signed URLs (expiration 1h):
const url = await s3.getSignedUrl('getObject', {
Bucket: 'ofm-content',
Key: 'content/xxx.jpg',
Expires: 3600, // 1 hour
});Watermarking automatique:
await sharp(inputBuffer)
.composite([{
input: watermarkBuffer,
gravity: 'southeast',
}])
.toFile(outputPath);.env files (JAMAIS commités):
# .gitignore
.env
.env.local
.env.*.localAWS Secrets Manager / HashiCorp Vault:
// ❌ Hard-coded secrets
const apiKey = 'sk_live_xxx';
// ✅ From secure store
const apiKey = await secretsManager.getSecret('stripe/api_key');Rotation:
- JWT secrets: Tous les 90 jours
- Database passwords: Tous les 6 mois
- API keys: On demand
Événements trackés:
enum AuditAction {
USER_LOGIN = 'user.login',
USER_LOGOUT = 'user.logout',
USER_REGISTER = 'user.register',
PAYMENT_CREATED = 'payment.created',
CONTENT_PUBLISHED = 'content.published',
SETTINGS_CHANGED = 'settings.changed',
}Stockage:
- Table
audit_log(PostgreSQL) - Retention: 1 an
- Export vers S3 pour compliance
Sentry:
- Capture tous les 5xx errors
- PII redacted automatiquement
- Source maps pour debugging
Logs structurés:
logger.error('Payment failed', {
userId: user.id,
amount: payment.amount,
error: err.message,
// NO sensitive data (card numbers, etc.)
});PhotoDNA / Google SafeSearch:
- Scan automatique de toutes les images
- Blocage immédiat si détecté
- Report aux autorités (NCMEC)
AI-based:
- TensorFlow.js model
- Classification: Safe / Questionable / Unsafe
- Manual review pour Questionable
DMCA Takedown:
- Form de signalement
- Review sous 24h
- Automatic takedown si vérifié
✅ Nous ne stockons JAMAIS les cartes bancaires
- Stripe gère 100% des paiements
- Tokens uniquement (non sensibles)
Droits des utilisateurs:
- ✅ Accès aux données:
GET /users/me/data-export - ✅ Portabilité: Export JSON/CSV
- ✅ Rectification:
PUT /users/me - ✅ Suppression:
DELETE /users/me
Cookie consent:
- Banner obligatoire
- Opt-in explicite
- Révocation facile
Vérification créateurs:
- Stripe Identity / Onfido
- Documents requis: ID + Proof of address
- Review sous 48h
- Revalidation annuelle
- Détection: Alertes automatiques (Sentry, CloudWatch)
- Isolation: Bloquer accès si nécessaire
- Investigation: Analyse logs, audit trail
- Mitigation: Patch, déploiement urgence
- Communication: Notification utilisateurs si breach
- Post-mortem: Document + amélioration
Security Team:
- Email: security@ofm.com
- PGP Key: [Publié sur site]
- Bug Bounty: HackerOne
- Firewall configuré (ports 80, 443 uniquement)
- WAF activé (Cloudflare)
- DDoS protection
- VPC network segmentation
- Bastion host pour accès DB
- SSH keys only (no password)
- Tous les secrets en variables d'env
- JWT secrets générés aléatoirement
- 2FA obligatoire pour admins
- Rate limiting activé
- Input validation complète
- Error messages génériques (no stack traces)
- Sentry configuré
- Logs centralisés
- Alertes configurées
- Backups automatiques quotidiens
- Disaster recovery testé
- RGPD privacy policy
- Terms of service
- Cookie consent
- DMCA agent registered
- 2257 compliance (si contenu adulte US)
Schedule:
- Dependencies: Scan quotidien (Dependabot)
- OS patches: Hebdomadaire
- Security audits: Trimestriel
- Penetration testing: Annuel
Process:
# Vérifier vulnérabilités
npm audit
# Update automatique (patch + minor)
npm update
# Review major versions manuellement
npm outdated- OWASP ZAP: Pen testing
- Burp Suite: Security testing
- SonarQube: Code quality & security
- Snyk: Dependency scanning
- OWASP Top 10
- NIST Cybersecurity Framework
- CIS Controls
- PCI-DSS (si applicable)
- OWASP Cheat Sheets: https://cheatsheetseries.owasp.org/
- Portswigger Academy: https://portswigger.net/web-security
Version: 1.0.0 Dernière mise à jour: 2024-01-XX Contact: security@ofm.com Bug Bounty: https://hackerone.com/ofm