Introduction
“Notre app est-elle sécurisée ?” Cette question revient souvent, suivie d’une longue liste d’inquiétudes parfois disproportionnées. Entre l’insouciance dangereuse et la paranoia paralysante, il existe un chemin pragmatique pour sécuriser efficacement une application web.
La sécurité n’est pas binaire. Il s’agit de gérer des risques selon leur probabilité et leur impact, en appliquant des mesures proportionnées aux menaces réelles.
Évaluation des risques : par où commencer
Le TOP 10 OWASP dans la vraie vie
Priorisation par impact réel
// Matrice risque/effort pour prioriser les actions sécurité
const securityRiskMatrix = {
CRITIQUE: {
impact: 'Très élevé',
likelihood: 'Élevée',
examples: [
'Injection SQL sur endpoint public',
'Authentication bypass',
'Exposition de données sensibles'
],
action: 'Fix immédiat, arrêt déploiements si nécessaire'
},
ÉLEVÉ: {
impact: 'Élevé',
likelihood: 'Moyenne',
examples: [
'XSS sur formulaires utilisateur',
'CSRF sur actions critiques',
'Permissions trop larges'
],
action: 'Fix dans la semaine, tests prioritaires'
},
MODÉRÉ: {
impact: 'Moyen',
likelihood: 'Faible',
examples: [
'Information disclosure mineure',
'Rate limiting insuffisant',
'Headers sécurité manquants'
],
action: 'Fix dans le sprint suivant'
},
FAIBLE: {
impact: 'Faible',
likelihood: 'Très faible',
examples: [
'Versions de libs légèrement obsolètes',
'Logs trop verbeux en dev',
'Crypto legacy mais fonctionnel'
],
action: 'Amélioration continue, pas urgent'
}
};
Audit rapide avec checklist
# 🔒 Security Checklist - Audit Express
## Authentication & Authorization
- [ ] Mots de passe hashés (bcrypt, scrypt, ou Argon2)
- [ ] Sessions sécurisées (httpOnly, secure, sameSite)
- [ ] Tokens JWT signés et avec expiration
- [ ] 2FA disponible pour admins/comptes sensibles
- [ ] Rate limiting sur login/register
## Input Validation
- [ ] Validation côté serveur systématique
- [ ] Paramètres SQL préparés (pas de concaténation)
- [ ] Échappement HTML pour affichage user content
- [ ] File upload restrictions (type, taille, scan virus)
- [ ] Validation des redirects (pas d'open redirect)
## Data Protection
- [ ] HTTPS partout (HSTS activé)
- [ ] Chiffrement des données sensibles en base
- [ ] Backup chiffrés
- [ ] Logs ne contiennent pas de secrets
- [ ] Variables d'environnement pour secrets
## Infrastructure
- [ ] Firewall correctement configuré
- [ ] Services internes non exposés publiquement
- [ ] Updates sécurité automatiques
- [ ] Monitoring tentatives d'intrusion
- [ ] Plan de réponse incident documenté
Contexte métier : adapter les mesures
// Sécurité proportionnée selon le contexte
const securityByContext = {
blogPersonnel: {
risques: ['Spam', 'Défacement', 'SEO spam'],
mesures: [
'Rate limiting basique',
'Validation inputs formulaires',
'HTTPS avec Let\'s Encrypt',
'Backup automatiques'
],
effort: 'Minimal, focus sur disponibilité'
},
eCommerce: {
risques: ['Vol données client', 'Fraude paiement', 'Manipulation prix'],
mesures: [
'PCI DSS compliance',
'Chiffrement bout en bout',
'Monitoring transactions suspectes',
'Audit logs détaillés'
],
effort: 'Élevé, investissement business critique'
},
startupB2B: {
risques: ['Accès données clients', 'IP theft', 'Service disruption'],
mesures: [
'RBAC granulaire',
'Encryption at rest',
'SOC2 préparation',
'Incident response plan'
],
effort: 'Progressif selon croissance'
},
fintech: {
risques: ['Régulation', 'Fraude massive', 'Réputation'],
mesures: [
'Multiple compliance frameworks',
'Zero-trust architecture',
'Continuous monitoring',
'Pentesting régulier'
],
effort: 'Maximum, coût de non-conformité énorme'
}
};
Authentification robuste
Stratégie de mots de passe moderne
Hashing sécurisé
// Configuration bcrypt/scrypt pour 2025
const passwordHashing = {
bcrypt: {
rounds: 12, // 2^12 iterations (adaptable selon hardware)
library: 'bcryptjs', // Pure JS pour compatibilité
example: async (password) => {
const saltRounds = 12;
return await bcrypt.hash(password, saltRounds);
}
},
scrypt: {
recommended: true,
config: {
N: 32768, // CPU/memory cost
r: 8, // Block size
p: 1, // Parallelization
keylen: 64 // Output length
},
example: (password, salt) => {
return new Promise((resolve, reject) => {
crypto.scrypt(password, salt, 64, { N: 32768, r: 8, p: 1 },
(err, derivedKey) => {
if (err) reject(err);
resolve(derivedKey);
});
});
}
}
};
// Implémentation pratique
class PasswordService {
async hashPassword(plainPassword) {
try {
// Générer salt aléatoire
const salt = crypto.randomBytes(32);
// Hash avec scrypt
const hashedPassword = await new Promise((resolve, reject) => {
crypto.scrypt(plainPassword, salt, 64, { N: 32768, r: 8, p: 1 },
(err, derivedKey) => {
if (err) reject(err);
resolve(derivedKey);
});
});
// Combiner salt + hash pour stockage
return {
hash: hashedPassword.toString('hex'),
salt: salt.toString('hex')
};
} catch (error) {
throw new Error('Password hashing failed');
}
}
async verifyPassword(plainPassword, storedHash, storedSalt) {
try {
const salt = Buffer.from(storedSalt, 'hex');
const hashedPassword = await new Promise((resolve, reject) => {
crypto.scrypt(plainPassword, salt, 64, { N: 32768, r: 8, p: 1 },
(err, derivedKey) => {
if (err) reject(err);
resolve(derivedKey);
});
});
return crypto.timingSafeEqual(
Buffer.from(storedHash, 'hex'),
hashedPassword
);
} catch (error) {
return false; // En cas d'erreur, refuse l'authentification
}
}
}
Politique de mots de passe équilibrée
// Validation moderne (NIST guidelines 2025)
const passwordPolicy = {
// Ce qu'on FAIT
requirements: {
minLength: 8,
maxLength: 128, // Pas de limite basse arbitraire
checkBreachedPasswords: true, // API HaveIBeenPwned
allowPassphrases: true,
allowPasswordManagers: true
},
// Ce qu'on ÉVITE (anciennes pratiques)
deprecated: {
complexityRules: false, // Pas d'obligation spéciaux/majuscules
regularExpiration: false, // Pas de changement forcé périodique
preventReuse: false, // Sauf cas très spécifiques
security_questions: false // Remplacés par 2FA
},
implementation: {
clientSideCheck: 'Feedback temps réel pour UX',
serverSideValidation: 'Validation finale obligatoire',
breachCheck: 'Async après validation basique',
userEducation: 'Guide pour password manager'
}
};
// Validation côté serveur
class PasswordValidator {
constructor() {
this.breachedPasswordsAPI = 'https://api.pwnedpasswords.com/range/';
}
async validate(password, userInfo = {}) {
const errors = [];
// Longueur
if (password.length < 8) {
errors.push('Password must be at least 8 characters');
}
if (password.length > 128) {
errors.push('Password too long (max 128 characters)');
}
// Pas d'infos personnelles évidentes
if (userInfo.email) {
const emailPart = userInfo.email.split('@')[0].toLowerCase();
if (password.toLowerCase().includes(emailPart)) {
errors.push('Password should not contain parts of your email');
}
}
// Check contre base de mots de passe compromis
try {
const isBreached = await this.checkBreachedPassword(password);
if (isBreached) {
errors.push('This password has been compromised in data breaches');
}
} catch (error) {
// En cas d'erreur API, on continue (ne pas bloquer l'utilisateur)
console.warn('Breach check failed:', error.message);
}
return {
valid: errors.length === 0,
errors,
strength: this.calculateStrength(password)
};
}
async checkBreachedPassword(password) {
// Hash SHA-1 du mot de passe
const hash = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();
const prefix = hash.substring(0, 5);
const suffix = hash.substring(5);
// API k-Anonymity de HaveIBeenPwned
const response = await fetch(`${this.breachedPasswordsAPI}${prefix}`);
const data = await response.text();
// Chercher le suffixe dans les résultats
return data.split('\n').some(line => line.startsWith(suffix));
}
}
Sessions et tokens sécurisés
Configuration session cookies
// Configuration Express sessions sécurisées
const sessionConfig = {
secret: process.env.SESSION_SECRET, // 256 bits minimum
name: 'sessionId', // Pas 'connect.sid' par défaut
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only
httpOnly: true, // Pas accessible en JavaScript
maxAge: 30 * 60 * 1000, // 30 minutes
sameSite: 'strict' // Protection CSRF basique
},
resave: false,
saveUninitialized: false,
store: new RedisStore({
client: redisClient,
prefix: 'sess:',
ttl: 1800 // 30 minutes en secondes
})
};
// Middleware de renouvellement session
const renewSession = (req, res, next) => {
if (req.session && req.session.user) {
// Renouveler la session si proche expiration
const timeLeft = req.session.cookie.maxAge;
if (timeLeft < 5 * 60 * 1000) { // 5 minutes restantes
req.session.cookie.maxAge = 30 * 60 * 1000; // Reset à 30 min
}
}
next();
};
JWT stratégie défensive
// Implémentation JWT sécurisée
class JWTService {
constructor() {
this.accessTokenSecret = process.env.JWT_ACCESS_SECRET;
this.refreshTokenSecret = process.env.JWT_REFRESH_SECRET;
this.issuer = 'myapp.com';
this.audience = 'myapp-users';
}
generateTokenPair(userId, roles = []) {
const now = Math.floor(Date.now() / 1000);
const accessToken = jwt.sign(
{
sub: userId,
roles,
type: 'access',
iat: now,
exp: now + (15 * 60), // 15 minutes
iss: this.issuer,
aud: this.audience
},
this.accessTokenSecret,
{ algorithm: 'HS256' }
);
const refreshToken = jwt.sign(
{
sub: userId,
type: 'refresh',
iat: now,
exp: now + (7 * 24 * 60 * 60), // 7 jours
iss: this.issuer,
aud: this.audience
},
this.refreshTokenSecret,
{ algorithm: 'HS256' }
);
return { accessToken, refreshToken };
}
async verifyAccessToken(token) {
try {
const payload = jwt.verify(token, this.accessTokenSecret, {
algorithms: ['HS256'],
issuer: this.issuer,
audience: this.audience
});
if (payload.type !== 'access') {
throw new Error('Invalid token type');
}
// Vérifier que l'utilisateur existe encore
const user = await this.getUserById(payload.sub);
if (!user || !user.active) {
throw new Error('User no longer active');
}
return payload;
} catch (error) {
throw new Error('Invalid access token');
}
}
async refreshTokens(refreshToken) {
try {
const payload = jwt.verify(refreshToken, this.refreshTokenSecret);
if (payload.type !== 'refresh') {
throw new Error('Invalid token type');
}
// Vérifier en base que le refresh token est valide
const storedToken = await this.getStoredRefreshToken(payload.sub, refreshToken);
if (!storedToken) {
throw new Error('Refresh token revoked or invalid');
}
// Révoquer l'ancien refresh token
await this.revokeRefreshToken(refreshToken);
// Générer nouveaux tokens
const user = await this.getUserById(payload.sub);
const newTokens = this.generateTokenPair(user.id, user.roles);
// Stocker le nouveau refresh token
await this.storeRefreshToken(user.id, newTokens.refreshToken);
return newTokens;
} catch (error) {
throw new Error('Invalid refresh token');
}
}
}
Protection des données sensibles
Chiffrement adapté aux besoins
Données au repos
// Stratégie chiffrement par type de données
const encryptionStrategy = {
// Très sensible : PII, secrets, tokens
highSensitive: {
algorithm: 'aes-256-gcm',
keyDerivation: 'PBKDF2',
implementation: class HighSensitiveEncryption {
constructor() {
this.masterKey = process.env.MASTER_ENCRYPTION_KEY;
}
encrypt(plaintext, associatedData = '') {
const salt = crypto.randomBytes(16);
const iv = crypto.randomBytes(12);
// Dériver clé de chiffrement
const key = crypto.pbkdf2Sync(this.masterKey, salt, 100000, 32, 'sha256');
const cipher = crypto.createCipher('aes-256-gcm', key);
cipher.setAAD(Buffer.from(associatedData));
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
encrypted,
salt: salt.toString('hex'),
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
};
}
}
},
// Modéré : données métier, logs
moderate: {
algorithm: 'aes-256-cbc',
note: 'Chiffrement simple mais efficace pour données moins critiques'
},
// Transit : toujours HTTPS + HSTS
inTransit: {
tls: 'TLS 1.3 minimum',
ciphers: 'Suites chiffrement modernes uniquement',
certificates: 'Let\'s Encrypt ou CA reconnu'
}
};
// Utilitaire pour champs sensibles en DB
class SensitiveField {
constructor(encryptionKey) {
this.encryptionKey = encryptionKey;
}
// Pour Sequelize/TypeORM
get(value) {
if (!value) return value;
return this.decrypt(value);
}
set(value) {
if (!value) return value;
return this.encrypt(value);
}
encrypt(plaintext) {
const cipher = crypto.createCipher('aes-256-cbc', this.encryptionKey);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
}
decrypt(ciphertext) {
const decipher = crypto.createDecipher('aes-256-cbc', this.encryptionKey);
let decrypted = decipher.update(ciphertext, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
Gestion des secrets
Variables d’environnement sécurisées
# .env.example - Template pour équipe
# Copier vers .env et remplir avec vraies valeurs
# Base de données
DATABASE_URL=postgresql://username:password@localhost:5432/dbname
DATABASE_SSL=true
# Authentification
JWT_ACCESS_SECRET=generate-256-bit-random-string
JWT_REFRESH_SECRET=generate-different-256-bit-string
SESSION_SECRET=another-256-bit-random-string
# Chiffrement
MASTER_ENCRYPTION_KEY=yet-another-256-bit-key
# APIs externes
STRIPE_SECRET_KEY=sk_test_...
SENDGRID_API_KEY=SG...
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...
# Environnement
NODE_ENV=development
LOG_LEVEL=debug
// Validation des secrets au démarrage
class ConfigValidator {
static validate() {
const required = [
'DATABASE_URL',
'JWT_ACCESS_SECRET',
'JWT_REFRESH_SECRET',
'SESSION_SECRET'
];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
console.error('Missing required environment variables:', missing);
process.exit(1);
}
// Vérifier la longueur des secrets
const secrets = ['JWT_ACCESS_SECRET', 'JWT_REFRESH_SECRET', 'SESSION_SECRET'];
secrets.forEach(secret => {
const value = process.env[secret];
if (Buffer.from(value).length < 32) { // 256 bits minimum
console.error(`${secret} must be at least 32 bytes (256 bits)`);
process.exit(1);
}
});
console.log('✅ Configuration validation passed');
}
}
// Au démarrage de l'app
ConfigValidator.validate();
Protection contre les attaques communes
Injection et validation d’entrée
Protection SQL Injection
// ✅ Bonnes pratiques requêtes sécurisées
class UserRepository {
constructor(db) {
this.db = db;
}
// Prepared statements (parameterized queries)
async findByEmail(email) {
const query = 'SELECT * FROM users WHERE email = $1 AND active = true';
const result = await this.db.query(query, [email]);
return result.rows[0];
}
// ORM avec validations
async updateUser(userId, data) {
// Validation avant requête
const validatedData = this.validateUserData(data);
// Query builder sécurisé (Knex exemple)
return await this.db('users')
.where('id', userId)
.update(validatedData);
}
validateUserData(data) {
const allowedFields = ['name', 'email', 'bio'];
const sanitized = {};
allowedFields.forEach(field => {
if (data[field] !== undefined) {
sanitized[field] = this.sanitizeString(data[field]);
}
});
return sanitized;
}
sanitizeString(str) {
if (typeof str !== 'string') return str;
// Supprimer caractères de contrôle
return str.replace(/[\x00-\x1F\x7F]/g, '')
.trim()
.substring(0, 1000); // Limite taille
}
}
Protection XSS
// Middleware anti-XSS global
const xssProtection = {
// Content Security Policy
csp: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
"'self'",
"'unsafe-inline'", // Éviter si possible
"https://cdn.jsdelivr.net",
"https://unpkg.com"
],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
imgSrc: ["'self'", "data:", "https:"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
connectSrc: ["'self'", "https://api.example.com"]
},
reportOnly: false // true pour mode test
},
// Headers sécurité
securityHeaders: (req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
}
};
// Sanitisation côté serveur
const DOMPurify = require('isomorphic-dompurify');
class ContentSanitizer {
static sanitizeHTML(dirty) {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href'],
ALLOW_DATA_ATTR: false
});
}
static sanitizeText(text) {
return text.replace(/[<>'"&]/g, (match) => {
const escapes = {
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'&': '&'
};
return escapes[match];
});
}
}
Rate limiting intelligent
// Rate limiting adaptatif selon contexte
class SmartRateLimiter {
constructor(redis) {
this.redis = redis;
this.rules = {
// Endpoints critiques
login: { window: 900, max: 5, punishment: 3600 }, // 15min window, 5 attempts, 1h ban
register: { window: 3600, max: 3, punishment: 7200 },
resetPassword: { window: 3600, max: 2, punishment: 3600 },
// APIs
apiPublic: { window: 60, max: 100, punishment: 300 },
apiAuth: { window: 60, max: 1000, punishment: 120 },
// Upload
fileUpload: { window: 3600, max: 10, punishment: 3600 }
};
}
async checkLimit(identifier, ruleKey, req = {}) {
const rule = this.rules[ruleKey];
if (!rule) return { allowed: true };
const key = `rate:${ruleKey}:${identifier}`;
const punishKey = `punish:${ruleKey}:${identifier}`;
// Vérifier si en punition
const isPunished = await this.redis.get(punishKey);
if (isPunished) {
return {
allowed: false,
reason: 'temporarily_banned',
retryAfter: await this.redis.ttl(punishKey)
};
}
// Compter les tentatives
const current = await this.redis.incr(key);
if (current === 1) {
await this.redis.expire(key, rule.window);
}
if (current > rule.max) {
// Déclencher punition
await this.redis.setex(punishKey, rule.punishment, 'banned');
// Log pour monitoring
console.warn('Rate limit exceeded', {
identifier,
rule: ruleKey,
attempts: current,
ip: req.ip,
userAgent: req.headers['user-agent']
});
return {
allowed: false,
reason: 'rate_limit_exceeded',
retryAfter: rule.punishment
};
}
return {
allowed: true,
remaining: rule.max - current,
resetTime: await this.redis.ttl(key)
};
}
// Middleware Express
createMiddleware(ruleKey, getIdentifier = (req) => req.ip) {
return async (req, res, next) => {
const identifier = getIdentifier(req);
const result = await this.checkLimit(identifier, ruleKey, req);
if (!result.allowed) {
return res.status(429).json({
error: 'Too Many Requests',
code: result.reason,
retryAfter: result.retryAfter
});
}
// Headers informatifs
res.set({
'X-RateLimit-Remaining': result.remaining,
'X-RateLimit-Reset': result.resetTime
});
next();
};
}
}
// Usage
const rateLimiter = new SmartRateLimiter(redisClient);
app.post('/auth/login',
rateLimiter.createMiddleware('login', req => req.ip),
loginController
);
app.post('/api/data',
authenticate, // Après auth pour avoir userId
rateLimiter.createMiddleware('apiAuth', req => req.userId),
apiController
);
Monitoring et réponse aux incidents
Détection d’activité suspecte
// Système d'alertes sécurité
class SecurityMonitor {
constructor(logger, alertService) {
this.logger = logger;
this.alertService = alertService;
this.suspiciousActivity = new Map(); // Cache pour patterns
}
// Middleware de monitoring
monitor() {
return (req, res, next) => {
const startTime = Date.now();
// Capturer la réponse pour analyse
const originalSend = res.send;
res.send = function(data) {
const responseTime = Date.now() - startTime;
// Analyser pattern d'attaque potentiel
this.analyzeRequest(req, res, responseTime, data);
originalSend.call(this, data);
}.bind(this);
next();
};
}
analyzeRequest(req, res, responseTime, responseData) {
const patterns = [
this.detectSQLInjection(req),
this.detectXSSAttempt(req),
this.detectDirectoryTraversal(req),
this.detectBruteForce(req, res),
this.detectScraping(req, responseTime)
];
const threats = patterns.filter(p => p.detected);
if (threats.length > 0) {
this.handleSecurityThreat(req, threats);
}
}
detectSQLInjection(req) {
const sqlPatterns = [
/(\bunion\b.*\bselect\b)|(\bselect\b.*\bunion\b)/i,
/(\bdrop\b.*\btable\b)|(\btable\b.*\bdrop\b)/i,
/'.*(\bor\b|\band\b).*'/i,
/\b(exec|execute|sp_|xp_)\b/i
];
const inputs = [
...Object.values(req.query || {}),
...Object.values(req.body || {}),
req.url
].join(' ');
const detected = sqlPatterns.some(pattern => pattern.test(inputs));
return {
type: 'sql_injection',
detected,
severity: 'high',
evidence: detected ? inputs : null
};
}
detectBruteForce(req, res) {
if (!req.url.includes('/auth/login')) return { detected: false };
const identifier = req.ip;
const key = `bf:${identifier}`;
let attempts = this.suspiciousActivity.get(key) || 0;
if (res.statusCode === 401) {
attempts++;
this.suspiciousActivity.set(key, attempts);
// Nettoyer après 15 minutes
setTimeout(() => {
this.suspiciousActivity.delete(key);
}, 15 * 60 * 1000);
}
return {
type: 'brute_force',
detected: attempts >= 10, // 10 échecs en 15 min
severity: 'medium',
evidence: `${attempts} failed login attempts`
};
}
async handleSecurityThreat(req, threats) {
const incident = {
timestamp: new Date().toISOString(),
ip: req.ip,
userAgent: req.headers['user-agent'],
url: req.url,
method: req.method,
threats,
userId: req.userId || null
};
// Log structuré
this.logger.warn('Security threat detected', incident);
// Alertes selon sévérité
const highSeverity = threats.some(t => t.severity === 'high');
if (highSeverity) {
await this.alertService.sendImmediate({
title: 'High Severity Security Alert',
description: `Potential ${threats[0].type} from ${req.ip}`,
incident
});
}
// Actions automatiques
await this.takeAutomatedAction(req.ip, threats);
}
async takeAutomatedAction(ip, threats) {
const highThreatTypes = ['sql_injection', 'xss'];
const hasHighThreat = threats.some(t => highThreatTypes.includes(t.type));
if (hasHighThreat) {
// Bannir temporairement l'IP
await this.redis.setex(`banned:${ip}`, 3600, 'auto_ban'); // 1 heure
this.logger.info('IP automatically banned', { ip, reason: 'high_threat' });
}
}
}
Plan de sécurisation progressive
Phase 1 : Fondations (Semaines 1-2)
- Audit de base : checklist sécurité rapide
- HTTPS partout : certificats SSL/TLS, HSTS
- Mots de passe : hashing sécurisé (scrypt/bcrypt)
- Variables d’environnement : externaliser tous les secrets
- Headers sécurité : CSP, X-Frame-Options, etc.
Phase 2 : Authentification (Semaines 3-4)
- Sessions sécurisées : configuration cookies, expiration
- JWT robuste : refresh tokens, révocation
- Rate limiting : protection brute force
- Validation entrées : sanitisation côté serveur
- Monitoring basique : logs tentatives suspectes
Phase 3 : Protection avancée (Mois 2)
- Chiffrement données : champs sensibles en base
- Backup sécurisés : chiffrement, rotation
- Dependency scanning : audit vulnérabilités npm
- RBAC : permissions granulaires
- Incident response : procédures documentées
Phase 4 : Excellence (Mois 3+)
- Pentest : audit externe ou interne
- Compliance : GDPR, SOC2 selon besoins
- Security training : formation équipe
- Bug bounty : programme de divulgation responsable
- Continuous monitoring : détection temps réel
Conclusion
La sécurité parfaite n’existe pas, mais une sécurité proportionnée et évolutive est à la portée de toute équipe. L’objectif n’est pas de se protéger contre toutes les attaques possibles, mais contre les attaques probables dans votre contexte.
Commencez par les fondations solides : HTTPS, authentification robuste, validation des entrées, monitoring basique. Ces mesures couvrent 80% des risques avec 20% de l’effort.
La sécurité n’est pas un état mais un processus. Chaque nouvelle fonctionnalité, chaque évolution d’architecture doit intégrer la dimension sécurité dès la conception.
Et rappelez-vous : une équipe sensibilisée vaut mieux que mille outils sophistiqués. Investissez dans la formation et la culture sécurité de vos équipes.
Quelle sera votre première action pour renforcer la sécurité de votre application ?