Cybersécurité

Sécuriser ses API : bonnes pratiques pour développeurs

11 دقائق للقراءة

Ce que vous saurez faire à la fin

  1. Sécuriser l’auth avec Argon2id + JWT rotatifs
  2. Implémenter OAuth 2.1 + PKCE
  3. Protéger contre OWASP Top 10
  4. Rate-limit et audit log immuable
  5. Scan secrets automatique en CI

Vue d’ensemble 1 — Argon2id

import argon2 from "argon2";

const hash = await argon2.hash(pwd, {
  type: argon2.argon2id,
  memoryCost: 65536,
  timeCost: 3,
  parallelism: 2,
});

if (!(await argon2.verify(hash, pwdFourni))) {
  await new Promise(r => setTimeout(r, 300));
  throw new Error("identifiants invalides");
}

Vue d’ensemble 2 — JWT avec refresh rotatif

import jwt from "jsonwebtoken";
import crypto from "crypto";

function issueTokens(user: { id: number }) {
  const access = jwt.sign({ sub: user.id }, process.env.ACCESS_SECRET!, 
    { expiresIn: "15m" });
  const jti = crypto.randomUUID();
  const refresh = jwt.sign({ sub: user.id, jti }, process.env.REFRESH_SECRET!,
    { expiresIn: "7d" });
  db.query("INSERT INTO refresh_tokens(jti,user_id,expires_at) VALUES($1,$2,now()+interval '7 days')",
    [jti, user.id]);
  return { access, refresh };
}

Vue d’ensemble 3 — Validation Zod

import { z } from "zod";
const CreerUser = z.object({
  email: z.string().email().max(255),
  telephone: z.string().regex(/^\+221 ?[0-9]{9}$/),
});

app.post("/users", (req, res) => {
  const r = CreerUser.safeParse(req.body);
  if (!r.success) return res.status(422).json({ errors: r.error.errors });
});

Vue d’ensemble 4 — Rate limiting

import { rateLimit } from "express-rate-limit";

const loginLimiter = rateLimit({
  windowMs: 15 * 60_000,
  max: 5,
  keyGenerator: req => req.ip + ":" + (req.body.email || ""),
});
app.post("/auth/login", loginLimiter, loginHandler);

Vue d’ensemble 5 — SQL paramétré

// JAMAIS
db.query(`SELECT * FROM users WHERE email='${req.body.email}'`);

// TOUJOURS
db.query("SELECT * FROM users WHERE email = $1", [req.body.email]);

Vue d’ensemble 6 — Anti-SSRF

const BLOQUES = [/^10\./, /^192\.168\./, /^127\./, /^169\.254\./];

async function fetchExterne(url: string) {
  const ip = await dns.lookup(new URL(url).hostname);
  if (BLOQUES.some(re => re.test(ip.address))) throw new Error("IP interne");
  return fetch(url, { redirect: "error" });
}

Vue d’ensemble 7 — CORS strict

import cors from "cors";
app.use(cors({
  origin: ["https://app.example.sn"],
  credentials: true,
}));

Vue d’ensemble 8 — Audit log immuable

CREATE TABLE audit_log (
  id BIGSERIAL PRIMARY KEY,
  ts TIMESTAMPTZ DEFAULT now(),
  user_id BIGINT,
  action TEXT NOT NULL,
  ressource TEXT NOT NULL,
  ip INET,
  result TEXT
);
REVOKE UPDATE, DELETE ON audit_log FROM PUBLIC;

Vue d’ensemble 9 — Webhooks signés

function verifyWebhook(req, secret: string) {
  const ts = parseInt(req.headers["x-timestamp"]);
  if (Math.abs(Date.now() - ts) > 5 * 60_000) throw new Error("stale");
  const expected = crypto.createHmac("sha256", secret)
    .update(`${ts}.${req.rawBody}`).digest("hex");
  if (!crypto.timingSafeEqual(Buffer.from(expected),
    Buffer.from(req.headers["x-signature"])))
    throw new Error("invalid signature");
}

Vue d’ensemble 10 — Scan secrets gitleaks

brew install gitleaks
gitleaks detect --source . --verbose

# Pre-commit hook
echo "gitleaks protect --staged --verbose" > .husky/pre-commit

Checklist

✓ Argon2id
✓ JWT 15 min + refresh 7j révocable
✓ Zod sur 100% des endpoints
✓ Rate limit login + global
✓ SQL paramétré uniquement
✓ CORS whitelist
✓ Audit log append-only
✓ gitleaks pre-commit
✓ Secrets dans vault

Hostinger pour vos premiers déploiements

Le panel hPanel reste l’un des plus accessibles du marché pour les débutants en self-hosting.

Découvrir hPanel →

Lien d affiliation. Si vous achetez via ce lien, le blog reçoit une petite commission sans surcoût pour vous.

Pourquoi la securite des API est critique en 2026

A Almadies comme a Yopougon, les fintechs et les SaaS B2B exposent des dizaines d’endpoints publics. Le rapport OWASP API Security Top 10 publie en 2023 et toujours d’actualite indique que 9 incidents sur 10 viennent de l’une de ces 10 categories. Aucune n’exige un attaquant sophistique : il suffit d’un script kiddie outille.

Ce tutoriel suit le cadre OWASP API 2023 (BOLA, Broken Authentication, Broken Object Property Level Authorization, Unrestricted Resource Consumption, etc.) et se concentre sur les actions concretes a prendre cette semaine sur votre API. Pas de theorie, du code et des commandes.

Etape 1 : authentifier solidement avec OAuth 2.1 ou JWT court

L’authentification basique (login/mot de passe en header) est a proscrire en 2026. Utilisez OAuth 2.1 pour les API publiques et des JWT courts (15 minutes max) avec refresh token rotatif pour les API internes. Le secret de signature doit etre une cle de 256 bits, jamais hardcodee.

// Node.js avec jsonwebtoken
import jwt from "jsonwebtoken";
const token = jwt.sign(
  { sub: user.id, role: user.role },
  process.env.JWT_SECRET,
  { expiresIn: "15m", algorithm: "HS256" }
);

Cote client, stockez le JWT en cookie HttpOnly Secure SameSite=Strict, jamais en localStorage qui est lisible par tout script XSS. Si une faille XSS existe, le localStorage devient une trappe a tokens.

Etape 2 : autoriser au niveau objet (BOLA)

BOLA (Broken Object Level Authorization) est le risque numero 1 du Top 10. L’utilisateur A authentifie peut acceder aux ressources de l’utilisateur B en changeant simplement un identifiant dans l’URL. Verifiez systematiquement la propriete avant chaque acces.

// Express + Prisma
app.get("/api/factures/:id", auth, async (req, res) => {
  const facture = await prisma.facture.findUnique({
    where: { id: req.params.id }
  });
  if (!facture || facture.userId !== req.user.id) {
    return res.status(404).json({ error: "Not Found" });
  }
  res.json(facture);
});

Notez le 404 plutot que 403 : ne revelez jamais a un attaquant qu’une ressource existe mais lui est interdite. C’est une difference subtile qui evite l’enumeration d’IDs.

Etape 3 : limiter le debit (rate limiting)

Sans rate limiting, un attaquant peut tester 100 000 mots de passe par minute, scraper toute votre base ou ddos le service. Mettez en place une limite par IP et par utilisateur authentifie. Redis est l’outil standard pour le compteur distribue.

// express-rate-limit avec Redis
import rateLimit from "express-rate-limit";
import RedisStore from "rate-limit-redis";
import { createClient } from "redis";

const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();

const limiter = rateLimit({
  store: new RedisStore({ sendCommand: (...args) => redis.sendCommand(args) }),
  windowMs: 60_000,
  max: 60,
  standardHeaders: true,
  legacyHeaders: false
});
app.use("/api/", limiter);

60 requetes par minute couvre 99 % des usages legitimes. Pour les endpoints sensibles (login, reset password), descendez a 5 par minute. Les requetes bloquees retournent 429 avec un header Retry-After.

Etape 4 : valider strictement les entrees

Toute donnee qui arrive du client est hostile par defaut. Validez schema, type, longueur, format. Une bibliotheque comme Zod (TypeScript) ou Pydantic (Python) automatise cette validation et rejette les payloads malformes avant qu’ils n’atteignent votre logique metier.

// Zod : refuse silencieusement les champs inconnus
import { z } from "zod";
const CreateUser = z.object({
  email: z.string().email().max(254),
  age: z.number().int().min(13).max(120),
  pseudo: z.string().regex(/^[a-zA-Z0-9_-]{3,30}$/)
}).strict();

const data = CreateUser.parse(req.body); // throws si invalide

Le mode .strict() rejette tout champ non declare. Cela bloque les attaques de type Mass Assignment ou un attaquant ajouterait isAdmin:true dans le body.

Etape 5 : chiffrer en transit et au repos

HTTPS partout, sans exception. Pour un VPS a Dakar ou Cotonou, utilisez Caddy ou Nginx avec Let’s Encrypt qui automatise le renouvellement. HSTS active sur 1 an minimum, prelaod a soumettre via hstspreload.org.

# Caddyfile minimal
api.exemple.com {
  reverse_proxy localhost:3000
  header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
  header X-Content-Type-Options nosniff
  header X-Frame-Options DENY
}

Au repos, chiffrez les colonnes sensibles (numero de telephone, identite, donnees medicales) avec AES-256-GCM. Les cles de chiffrement vivent dans un KMS (AWS KMS, Google Cloud KMS, ou HashiCorp Vault auto-heberge), jamais dans le code ni dans les variables d’environnement de production sans rotation.

Etape 6 : journaliser et detecter les anomalies

Sans logs, vous decouvrez une intrusion 6 mois apres les faits via Have I Been Pwned. Avec des logs structures et un SIEM (Security Information and Event Management), vous detectez en quelques minutes. Loki ou Elasticsearch suffisent pour commencer.

// Pino : log JSON haute performance
import pino from "pino";
const log = pino({ level: "info" });
log.info({ user: req.user.id, ip: req.ip, route: req.path }, "api_call");
log.warn({ user: req.user.id, ip: req.ip }, "auth_failed");

Configurez des alertes : 5 echecs de login en 1 minute depuis la meme IP, 100 requetes 4xx en 1 minute, 1 acces a un endpoint admin par un compte non-admin. Chaque alerte declenche un Slack ou un email a l’equipe SecOps.

Etape 7 : tester avec un audit OWASP ZAP automatise

Avant chaque mise en production, lancez ZAP en mode baseline scan dans votre pipeline CI. Le scan detecte les en-tetes manquants, les cookies non securises, les endpoints debug exposes et bien plus. Le rapport HTML est lisible et actionnable.

# GitHub Actions : scan ZAP a chaque PR
- name: ZAP Baseline Scan
  uses: zaproxy/action-baseline@v0.13.0
  with:
    target: https://staging.api.exemple.com
    fail_action: true

Une PR qui introduit une regression de securite est bloquee automatiquement. Compre cette etape avec un test manuel mensuel : un developpeur senior simule 30 minutes d’attaque sur l’API en pre-prod. Sur un angle proche sur les bonnes pratiques de structure web, voyez notre guide structuration des URLs et les erreurs SEO a eviter qui partagent les memes principes de defense en profondeur.

Etape 8 : gerer les secrets sans les commiter

Les fuites de cles API sur GitHub representent 60 % des incidents en 2026. Activez GitGuardian ou TruffleHog en pre-commit hook pour bloquer tout secret avant le push. Utilisez direnv ou dotenv-vault pour les environnements locaux, AWS Secrets Manager ou HashiCorp Vault pour la production.

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/trufflesecurity/trufflehog
    rev: v3.78.0
    hooks:
      - id: trufflehog
        entry: trufflehog git file://. --since-commit HEAD --fail

Si un secret a deja fuite, ne le supprimez pas seulement du depot : revoquez-le immediatement chez le fournisseur. Un attaquant peut avoir cloner le repo entre le push et le retrait. La revocation est l’unique mesure efficace.

Etape 9 : sceller les en-tetes de reponse HTTP

Les navigateurs offrent une defense gratuite via les en-tetes Content-Security-Policy, Permissions-Policy, X-Content-Type-Options et Cross-Origin-Opener-Policy. Mal configures, ces en-tetes laissent passer XSS, clickjacking et fuite cross-origin. Utilisez helmet sur Express ou la couche middleware equivalente.

import helmet from "helmet";
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'nonce-{{cspNonce}}'"],
      styleSrc: ["'self'"],
      imgSrc: ["'self'", "data:"],
      connectSrc: ["'self'"]
    }
  },
  crossOriginEmbedderPolicy: true
}));

Verifiez la configuration sur securityheaders.com. Visez le grade A. Un grade B ou inferieur indique au minimum un en-tete manquant ou trop permissif. Le test prend 30 secondes et le rapport est lisible.

Etape 10 : limiter les payloads et la pagination

Sans limite de taille de body, un attaquant envoie un JSON de 500 Mo et fait tomber le service. Sans pagination, une requete GET /api/users retourne 200 000 lignes et sature la memoire. Imposez les limites cote framework et cote reverse proxy.

app.use(express.json({ limit: "100kb" }));

app.get("/api/users", auth, async (req, res) => {
  const limit = Math.min(parseInt(req.query.limit) || 20, 100);
  const cursor = req.query.cursor;
  const users = await prisma.user.findMany({
    take: limit + 1,
    cursor: cursor ? { id: cursor } : undefined,
    skip: cursor ? 1 : 0,
    orderBy: { id: "asc" }
  });
  const next = users.length > limit ? users.pop().id : null;
  res.json({ data: users, next });
});

La pagination par curseur est plus solide que par offset : elle reste rapide meme sur les pages 1000+ et resiste aux insertions concurrentes. Un offset 100 000 sur une grosse table prend plusieurs secondes, un curseur reste sous la milliseconde.

Etape 11 : gerer les CORS sans tout ouvrir

Les regles CORS mal configurees sont une source d’incidents recurrents. Access-Control-Allow-Origin: * combine a Allow-Credentials: true est une faute grave qui expose les sessions des utilisateurs a tout site tiers. Limitez les origines a une whitelist explicite.

import cors from "cors";
const origins = ["https://app.exemple.com", "https://admin.exemple.com"];
app.use(cors({
  origin: (origin, cb) => {
    if (!origin || origins.includes(origin)) cb(null, true);
    else cb(new Error("CORS bloque"));
  },
  credentials: true,
  maxAge: 600
}));

Un test rapide depuis la console d’un navigateur sur un domaine tiers detecte les configurations laxistes. Si la requete passe alors qu’elle ne devrait pas, corrigez avant la mise en production.

Etape 12 : prevoir une procedure de reponse a incident

Le jour ou une fuite arrive, vous n’aurez pas le temps d’improviser. Documentez en amont : qui revoque les cles, qui contacte les utilisateurs touches, qui rediger le communique, qui notifie la CDP du Senegal ou la CNIL ivoirienne dans les 72 heures legales. Cette documentation tient sur une page A4.

Faites un exercice tabletop trimestriel : simulez une fuite (par exemple une cle AWS commitee), chronometrez chaque etape. Les premieres fois, le delai depasse 4 heures. Apres 3 exercices, vous descendez sous 45 minutes. Cette difference de vitesse evite des amendes RGPD a 4 % du chiffre d’affaires mondial.

Etape 13 : suivre les vulnerabilites des dependances

Vos dependances NPM, Pip ou Cargo introduisent du code que vous n’avez pas ecrit. Un paquet compromis devient une porte derobee dans votre API. Activez Dependabot ou Renovate sur GitHub pour recevoir une PR automatique a chaque CVE publiee. Verifiez chaque mise a jour avant de merger : un changelog suspect doit etre etudie.

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10

Combinez avec un audit hebdomadaire npm audit --audit-level=high ou pip-audit. Toute faille de niveau eleve ou critique se patche dans la semaine, pas dans le trimestre. La fenetre d’exploitation moyenne pour une CVE publique est de 8 jours en 2026, contre 30 jours il y a 5 ans.

مشاركة