Sécuriser un VPS Debian 12 pour WordPress : checklist pratique 2026
WordPress attire les bots. Pas parce que WordPress est particulièrement vulnérable — un WordPress à…
Guide complet de sécurité des API basé sur l'OWASP API Security Top 10 : authentication, authorization, protection contre les vulnérabilités et meilleures pratiques 2025.
Les API sont devenues la colonne vertébrale des applications modernes, mais elles représentent aussi une surface d’attaque critique. L’OWASP API Security Top 10 (édition 2023, toujours pertinent en 2025) identifie les vulnérabilités les plus critiques. Ce guide complet vous présente ces risques et les meilleures pratiques pour sécuriser vos APIs.
Les deux risques de sécurité API principaux identifiés par l’OWASP sont l’autorisation au niveau objet cassée (Broken Object Level Authorization) et l’authentification cassée (Broken Authentication).
L’utilisateur peut accéder à des objets (ressources) appartenant à d’autres utilisateurs en manipulant l’ID dans l’URL.
Exemple de vulnérabilité :
GET /api/users/123/orders/456
Authorization: Bearer user789>
# User 789 peut voir les commandes de User 123 !
Les vérifications d’autorisation au niveau objet doivent être considérées dans chaque fonction qui accède à une source de données en utilisant un ID provenant de l’utilisateur.
Code vulnérable (Node.js/Express) :
// ❌ VULNÉRABLE : Pas de vérification de propriété
app.get('/api/orders/:orderId', async (req, res) => {
const order = await Order.findById(req.params.orderId);
if (!order) {
return res.status(404).json({ error: 'Order not found' });
}
// DANGER : Renvoie la commande sans vérifier si elle appartient à l'utilisateur !
res.json(order);
});
Code sécurisé :
// ✅ SÉCURISÉ : Vérification de propriété
app.get('/api/orders/:orderId', authenticateUser, async (req, res) => {
const order = await Order.findById(req.params.orderId);
if (!order) {
return res.status(404).json({ error: 'Order not found' });
}
// Vérifier que la commande appartient à l'utilisateur authentifié
if (order.userId !== req.user.id) {
return res.status(403).json({ error: 'Forbidden: Access denied' });
}
res.json(order);
});
Approche middleware réutilisable :
// Middleware de vérification de propriété
function checkOwnership(resourceModel, userIdField = 'userId') {
return async (req, res, next) => {
const resourceId = req.params.id || req.params.resourceId;
const resource = await resourceModel.findById(resourceId);
if (!resource) {
return res.status(404).json({ error: 'Resource not found' });
}
if (resource[userIdField] !== req.user.id) {
return res.status(403).json({ error: 'Forbidden: Access denied' });
}
// Attacher la ressource à la requête
req.resource = resource;
next();
};
}
// Utilisation
app.get('/api/orders/:id',
authenticateUser,
checkOwnership(Order, 'userId'),
(req, res) => {
res.json(req.resource);
}
);
Appliquez des vérifications d’autorisation au niveau objet sur chaque endpoint, validez les permissions utilisateur avant les actions de lecture, mise à jour ou suppression, et utilisez le contrôle d’accès basé sur les rôles (RBAC) ou basé sur les attributs (ABAC).
RBAC (Role-Based Access Control) :
// Définir les rôles et permissions
const ROLES = {
ADMIN: 'admin',
USER: 'user',
GUEST: 'guest'
};
const PERMISSIONS = {
'order:read:own': [ROLES.USER, ROLES.ADMIN],
'order:read:all': [ROLES.ADMIN],
'order:write:own': [ROLES.USER, ROLES.ADMIN],
'order:delete:any': [ROLES.ADMIN]
};
function checkPermission(permission) {
return (req, res, next) => {
const userRole = req.user.role;
if (!PERMISSIONS[permission]?.includes(userRole)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Utilisation
app.get('/api/orders',
authenticateUser,
checkPermission('order:read:own'),
async (req, res) => {
const orders = await Order.find({ userId: req.user.id });
res.json(orders);
}
);
Utilisez OIDC pour l’authentification/SSO ; utilisez OAuth pour l’autorisation vers les APIs. Utilisez des standards sécurisés comme OAuth 2.0 ou OpenID Connect et implémentez l’authentification multi-facteurs (MFA).
OpenID Connect (OIDC) – Flux d’authentification :
// Exemple avec Passport.js et OIDC
const passport = require('passport');
const { Strategy: OIDCStrategy } = require('passport-openidconnect');
passport.use('oidc', new OIDCStrategy({
issuer: 'https://accounts.google.com',
authorizationURL: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenURL: 'https://oauth2.googleapis.com/token',
userInfoURL: 'https://openidconnect.googleapis.com/v1/userinfo',
clientID: process.env.GOOGLECLIENTID,
clientSecret: process.env.GOOGLECLIENTSECRET,
callbackURL: 'https://monapp.com/auth/callback',
scope: ['openid', 'profile', 'email']
},
(issuer, profile, done) => {
// Vérifier/créer l'utilisateur dans votre DB
User.findOrCreate({ googleId: profile.id }, (err, user) => {
return done(err, user);
});
}
));
// Routes
app.get('/auth/google',
passport.authenticate('oidc')
);
app.get('/auth/callback',
passport.authenticate('oidc', {
successRedirect: '/dashboard',
failureRedirect: '/login'
})
);
Validez les tokens ID sur la partie dépendante : émetteur (iss), audience (aud), signature (selon les JWKs du fournisseur), expiration (exp).
Validation JWT complète :
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
// Client pour récupérer les clés publiques du fournisseur
const client = jwksClient({
jwksUri: 'https://accounts.google.com/.well-known/jwks.json',
cache: true,
cacheMaxAge: 86400000 // 24h
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) {
callback(err);
} else {
const signingKey = key.getPublicKey();
callback(null, signingKey);
}
});
}
function validateToken(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
jwt.verify(token, getKey, {
// Valider l'émetteur
issuer: 'https://accounts.google.com',
// Valider l'audience (votre client ID)
audience: process.env.GOOGLECLIENTID,
// Algorithmes autorisés
algorithms: ['RS256']
}, (err, decoded) => {
if (err) {
return res.status(401).json({ error: 'Invalid token', details: err.message });
}
// Vérifier l'expiration (exp) - fait automatiquement par jwt.verify
// Vérifier additional claims si nécessaire
if (!decoded.emailverified) {
return res.status(403).json({ error: 'Email not verified' });
}
req.user = decoded;
next();
});
}
app.get('/api/protected',
validateToken,
(req, res) => {
res.json({ message: 'Protected data', user: req.user });
}
);
Utilisez des access tokens de courte durée pour minimiser les risques en implémentant des tokens avec des durées de vie limitées.
Implémentation Refresh Token Pattern :
const crypto = require('crypto');
// Générer les tokens
function generateTokens(user) {
// Access token : courte durée (15 minutes)
const accessToken = jwt.sign(
{
sub: user.id,
email: user.email,
role: user.role
},
process.env.JWTSECRET,
{
expiresIn: '15m',
issuer: 'monapp.com',
audience: 'api.monapp.com'
}
);
// Refresh token : longue durée (7 jours), stocké en DB
const refreshToken = crypto.randomBytes(40).toString('hex');
// Stocker le refresh token en DB avec expiration
RefreshToken.create({
token: refreshToken,
userId: user.id,
expiresAt: new Date(Date.now() + 7 24 60 60 1000) // 7 jours
});
return { accessToken, refreshToken };
}
// Endpoint de refresh
app.post('/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({ error: 'Refresh token required' });
}
// Vérifier le refresh token en DB
const storedToken = await RefreshToken.findOne({
token: refreshToken,
expiresAt: { $gt: new Date() }
});
if (!storedToken) {
return res.status(401).json({ error: 'Invalid or expired refresh token' });
}
// Générer de nouveaux tokens
const user = await User.findById(storedToken.userId);
const tokens = generateTokens(user);
// Supprimer l'ancien refresh token (rotation)
await RefreshToken.deleteOne({ id: storedToken.id });
res.json(tokens);
});
Forcez l’authentification multi-facteurs en exigeant plusieurs étapes de vérification pour l’authentification utilisateur.
Implémentation TOTP (Time-based One-Time Password) :
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// Setup MFA pour un utilisateur
app.post('/api/user/mfa/setup', authenticateUser, async (req, res) => {
// Générer un secret
const secret = speakeasy.generateSecret({
name: MonApp (${req.user.email}),
issuer: 'MonApp'
});
// Sauvegarder le secret temporaire (non confirmé)
await User.findByIdAndUpdate(req.user.id, {
mfaSecretTemp: secret.base32,
mfaEnabled: false
});
// Générer le QR code pour l'app authenticator
const qrCodeUrl = await QRCode.toDataURL(secret.otpauthurl);
res.json({
secret: secret.base32,
qrCode: qrCodeUrl
});
});
// Vérifier et activer MFA
app.post('/api/user/mfa/verify', authenticateUser, async (req, res) => {
const { token } = req.body;
const user = await User.findById(req.user.id);
// Vérifier le token TOTP
const verified = speakeasy.totp.verify({
secret: user.mfaSecretTemp,
encoding: 'base32',
token: token,
window: 2 // Accepter ±2 périodes (30s chacune)
});
if (!verified) {
return res.status(400).json({ error: 'Invalid verification code' });
}
// Activer MFA
await User.findByIdAndUpdate(req.user.id, {
mfaSecret: user.mfaSecretTemp,
mfaEnabled: true,
$unset: { mfaSecretTemp: 1 }
});
res.json({ message: 'MFA enabled successfully' });
});
// Middleware de vérification MFA au login
function requireMFA(req, res, next) {
if (!req.user.mfaEnabled) {
return next();
}
const { mfaToken } = req.body;
if (!mfaToken) {
return res.status(401).json({
error: 'MFA token required',
mfaRequired: true
});
}
const verified = speakeasy.totp.verify({
secret: req.user.mfaSecret,
encoding: 'base32',
token: mfaToken,
window: 2
});
if (!verified) {
return res.status(401).json({ error: 'Invalid MFA token' });
}
next();
}
Implémentez le rate limiting et les politiques de verrouillage pour protéger contre le brute force et le credential stuffing.
const rateLimit = require('express-rate-limit'); const RedisStore = require('rate-limit-redis'); const Redis = require('ioredis'); const redis = new Redis(process.env.REDISURL); // Rate limiter pour login const loginLimiter = rateLimit({ store: new RedisStore({ client: redis, prefix: 'ratelimit:login:' }), windowMs: 15 60 1000, // 15 minutes max: 5, // 5 tentatives max message: 'Too many login attempts, please try again after 15 minutes', standardHeaders: true, legacyHeaders: false, skipSuccessfulRequests: true, // Ne pas compter les logins réussis handler: (req, res) => { res.status(429).json({ error: 'Too many login attempts', retryAfter: res.getHeader('Retry-After') }); } }); // Account lockout après échecs répétés async function checkAccountLockout(userId) { const key =; const attempts = await redis.get(key); if (attempts && parseInt(attempts) >= 10) { const ttl = await redis.ttl(key); throw new Error(accountlockout:${userId}Account locked. Try again in ${Math.ceil(ttl / 60)} minutes); } } async function recordFailedAttempt(userId) { const key =accountlockout:${userId}; const attempts = await redis.incr(key); if (attempts === 1) { // Premier échec : expiration dans 1 heure await redis.expire(key, 3600); } if (attempts >= 10) { // 10 échecs : verrouiller pour 24h await redis.expire(key, 86400); } } async function resetFailedAttempts(userId) { const key =accountlockout:${userId}; await redis.del(key); } // Route de login avec protection app.post('/auth/login', loginLimiter, async (req, res) => { const { email, password } = req.body; try { const user = await User.findOne({ email }); if (!user) { return res.status(401).json({ error: 'Invalid credentials' }); } // Vérifier le verrouillage du compte await checkAccountLockout(user.id); // Vérifier le mot de passe const valid = await bcrypt.compare(password, user.passwordHash); if (!valid) { await recordFailedAttempt(user.id); return res.status(401).json({ error: 'Invalid credentials' }); } // Login réussi : réinitialiser les tentatives await resetFailedAttempts(user.id); const tokens = generateTokens(user); res.json(tokens); } catch (error) { res.status(403).json({ error: error.message }); } });
Les services REST sécurisés ne doivent fournir que des endpoints HTTPS pour protéger les credentials d’authentification en transit, par exemple mots de passe, clés API ou JSON Web Tokens.
Configuration HTTPS (Node.js) :
const https = require('https');
const fs = require('fs');
const express = require('express');
const app = express();
// Rediriger HTTP vers HTTPS
const httpApp = express();
httpApp.use((req, res) => {
res.redirect(301, https://${req.headers.host}${req.url});
});
httpApp.listen(80);
// Options HTTPS
const httpsOptions = {
key: fs.readFileSync('/etc/ssl/private/key.pem'),
cert: fs.readFileSync('/etc/ssl/certs/cert.pem'),
ca: fs.readFileSync('/etc/ssl/certs/chain.pem'),
// Sécurité renforcée
minVersion: 'TLSv1.2',
ciphers: [
'ECDHE-ECDSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-AES256-GCM-SHA384',
'ECDHE-RSA-AES256-GCM-SHA384'
].join(':'),
honorCipherOrder: true
};
https.createServer(httpsOptions, app).listen(443);
Headers de sécurité :
const helmet = require('helmet');
app.use(helmet({
// HSTS : Force HTTPS pendant 1 an
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
// Empêcher le sniffing de MIME type
noSniff: true,
// Protection XSS
xssFilter: true,
// Empêcher le clickjacking
frameguard: { action: 'deny' },
// Content Security Policy
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"]
}
}
}));
Implémentez une validation et une sanitization robustes des entrées et appliquez une validation stricte des entrées pour prévenir les attaques par injection.
Validation avec Joi :
const Joi = require('joi');
// Schéma de validation
const userSchema = Joi.object({
email: Joi.string()
.email()
.required()
.max(255),
password: Joi.string()
.min(12)
.max(128)
.pattern(/^(?=.[a-z])(?=.[A-Z])(?=.d)(?=.[@$!%?&])[A-Za-zd@$!%?&]/)
.required()
.messages({
'string.pattern.base': 'Password must contain uppercase, lowercase, number, and special character'
}),
name: Joi.string()
.min(2)
.max(100)
.pattern(/^[a-zA-Zs'-]+$/)
.required(),
age: Joi.number()
.integer()
.min(18)
.max(150)
.optional()
});
// Middleware de validation
function validateBody(schema) {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, {
abortEarly: false, // Retourner toutes les erreurs
stripUnknown: true // Supprimer les champs inconnus
});
if (error) {
const errors = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
return res.status(400).json({ errors });
}
// Remplacer req.body par les données validées
req.body = value;
next();
};
}
// Utilisation
app.post('/api/users',
validateBody(userSchema),
async (req, res) => {
// req.body est maintenant validé et sanitizé
const user = await User.create(req.body);
res.status(201).json(user);
}
);
Protection contre les injections SQL :
// ❌ VULNÉRABLE à SQL Injection
app.get('/api/users', (req, res) => {
const query = SELECT FROM users WHERE name = '${req.query.name}';
db.query(query); // DANGER!
});
// ✅ SÉCURISÉ : Requêtes paramétrées
app.get('/api/users', (req, res) => {
const query = 'SELECT FROM users WHERE name = ?';
db.query(query, [req.query.name]); // Paramètres échappés automatiquement
});
// ✅ Avec ORM (Sequelize)
app.get('/api/users', async (req, res) => {
const users = await User.findAll({
where: { name: req.query.name } // Requête paramétrée automatiquement
});
res.json(users);
});
Protection contre NoSQL Injection :
// ❌ VULNÉRABLE à NoSQL Injection
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
// Si req.body.username = { $ne: null }, bypass authentication!
const user = await User.findOne({ username, password });
});
// ✅ SÉCURISÉ : Validation de type
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
// Forcer les types string
if (typeof username !== 'string' || typeof password !== 'string') {
return res.status(400).json({ error: 'Invalid input types' });
}
const user = await User.findOne({
username: username,
password: password
});
});
// ✅ Utiliser mongoose-sanitize
const mongoSanitize = require('express-mongo-sanitize');
app.use(mongoSanitize({
replaceWith: '' // Remplacer $ et . par
}));
Implémentez le rate limiting pour empêcher les abus.
Rate limiting par endpoint :
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
// Rate limiter global
const globalLimiter = rateLimit({
store: new RedisStore({ client: redis }),
windowMs: 15 60 1000, // 15 minutes
max: 1000, // 1000 requêtes max par IP
message: 'Too many requests from this IP'
});
// Rate limiter stricte pour endpoints sensibles
const authLimiter = rateLimit({
store: new RedisStore({ client: redis }),
windowMs: 15 60 1000,
max: 5, // 5 requêtes max
skipSuccessfulRequests: true
});
// Rate limiter modéré pour API
const apiLimiter = rateLimit({
store: new RedisStore({ client: redis }),
windowMs: 1 60 1000, // 1 minute
max: 100 // 100 requêtes/minute
});
// Application
app.use('/api/', globalLimiter);
app.use('/auth/', authLimiter);
app.use('/api/', apiLimiter);
Rate limiting par utilisateur authentifié :
const userRateLimiter = rateLimit({
store: new RedisStore({ client: redis }),
windowMs: 60 1000, // 1 minute
max: async (req) => {
// Limites basées sur le plan utilisateur
if (req.user.plan === 'enterprise') {
return 10000; // 10k req/min
} else if (req.user.plan === 'pro') {
return 1000; // 1k req/min
}
return 100; // Plan gratuit : 100 req/min
},
keyGenerator: (req) => {
// Utiliser l'user ID au lieu de l'IP
return req.user ? req.user.id : req.ip;
}
});
Surveillez l’utilisation de l’API pour détecter les activités suspectes.
Logging sécurisé :
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// Middleware de logging
function logRequest(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('API Request', {
method: req.method,
url: req.url,
status: res.statusCode,
duration: ${duration}ms,
ip: req.ip,
userId: req.user?.id,
userAgent: req.get('user-agent'),
// NE JAMAIS logger les credentials ou tokens!
headers: sanitizeHeaders(req.headers)
});
});
next();
}
function sanitizeHeaders(headers) {
const sanitized = { ...headers };
delete sanitized.authorization;
delete sanitized.cookie;
return sanitized;
}
app.use(logRequest);
Détection d’anomalies :
const anomalyDetector = {
async checkSuspiciousActivity(req) {
const userId = req.user?.id || req.ip;
const key = activity:${userId};
// Nombre de requêtes dans la dernière minute
const count = await redis.incr(key);
await redis.expire(key, 60);
if (count > 1000) {
// Alerte : Trop de requêtes
logger.warn('Suspicious activity detected', {
userId,
ip: req.ip,
requestCount: count
});
// Notifier l'équipe de sécurité
await alertSecurityTeam({
type: 'highrequestrate',
userId,
count
});
return false;
}
return true;
}
};
app.use(async (req, res, next) => {
const allowed = await anomalyDetector.checkSuspiciousActivity(req);
if (!allowed) {
return res.status(429).json({ error: 'Suspicious activity detected' });
}
next();
});
Authentication & Authorization :
Object-Level Authorization :
Communication :
Validation :
Rate Limiting :
Monitoring :
Secrets Management :
La sécurité des API en 2025 exige une approche multi-couches couvrant l’authentification, l’autorisation, la communication, la validation et le monitoring. Ces meilleures pratiques reflètent les dernières directives OWASP API Security Top 10 (2023) et restent hautement pertinentes pour 2025.
Points clés à retenir :
L’OWASP fournit des ressources complètes, des cheat sheets et une communauté active pour vous aider à sécuriser vos APIs. Restez à jour avec les dernières menaces et meilleures pratiques.
Article mis à jour en décembre 2025 avec les dernières recommandations OWASP et pratiques de sécurité API.*
Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.