OWASP & AppSec

Principes de base du secure coding : 12 règles avec code

12 principes de secure coding illustrés par du code Python, TypeScript, Java, Go : fail closed, timing-safe, SSRF allowlist, TOCTOU, désérialisation, ORM.

Naim Aouaichia
15 min de lecture
  • Secure coding
  • OWASP
  • ASVS
  • Exemples de code
  • Développement sécurisé
  • Vulnérabilités
  • Patterns

Les principes de secure coding sont les règles d'ingénierie qui préviennent mécaniquement les vulnérabilités à la source, indépendamment du langage. Cet article détaille 12 principes opérationnels illustrés par du code Python, TypeScript, Java et Go — en allant au-delà des exemples évidents (injection SQL paramétrée, XSS avec encodage HTML) pour couvrir les patterns où les développeurs expérimentés échouent le plus souvent : comparaison timing-safe de secrets, TOCTOU / race conditions, désérialisation sûre, SSRF en allowlist, mass assignment ORM, prototype pollution côté Node.js, logs sans PII ni secrets, TOCTOU. Les principes sont alignés OWASP ASVS v4.0.3 (sections V5 Validation, V6 Cryptography, V7 Error Handling, V8 Data Protection, V10 Malicious Code), NIST SP 800-218 SSDF (Secure Software Development Framework), NIST SP 800-53 SI-10, et les CWE Top 25 2023-2024. Un codebase qui applique rigoureusement ces 12 principes prévient 80 à 90 % des vulnérabilités du Top 10 OWASP 2021 sans avoir à les énumérer une par une. Pour la version parcours d'apprentissage structuré, voir Roadmap secure coding ; pour le catalogue des vulnérabilités classiques, Introduction OWASP Top 10.

1. Fail Closed / Secure by Default

Un système doit refuser l'accès en cas de doute plutôt que l'autoriser. Principe aligné CWE-636 (Not Failing Securely) et ASVS V1.4.1. L'erreur la plus fréquente : écrire une fonction d'autorisation qui retourne true par défaut ou qui retourne l'autorisation sur exception au lieu de la refuser.

Contre-exemple classique (Python)

def can_user_access(user, resource) -> bool:
    try:
        policy = load_policy(resource)
        return policy.allows(user)
    except Exception as e:
        logger.warning(f"Policy load failed: {e}")
        return True  # ❌ fail open : accès accordé sur erreur

Version correcte

def can_user_access(user, resource) -> bool:
    try:
        policy = load_policy(resource)
    except Exception as e:
        logger.error(
            "policy_load_failed",
            extra={"resource": resource.id, "user": user.id, "err": str(e)}
        )
        return False  # ✅ fail closed
    return policy.allows(user)

2. Valider à la Trust Boundary, pas partout

Le modèle Trust Boundary OWASP définit que la validation se fait à l'entrée du périmètre de confiance (HTTP handler, message queue consumer, CLI args, import file), puis les données validées circulent sous forme de types qui portent l'invariant. Éparpiller la validation partout produit du code bruité et des invariants inconsistants.

Pattern parse-don't-validate (TypeScript avec Zod)

import { z } from "zod";
 
// Schéma qui PARSE et TYPE la donnée à la frontière
const CreateOrderInput = z.object({
  customerId: z.string().uuid(),
  items: z.array(z.object({
    sku: z.string().regex(/^[A-Z0-9-]{3,20}$/),
    quantity: z.number().int().min(1).max(1000),
  })).min(1).max(50),
  shippingAddress: z.object({
    country: z.enum(["FR", "BE", "CH", "LU", "MC"]),
    postalCode: z.string().regex(/^\d{5}$/),
  }),
});
 
type CreateOrderInput = z.infer<typeof CreateOrderInput>;
 
// HTTP handler = seule frontière où on valide
app.post("/api/orders", async (req, res) => {
  const parsed = CreateOrderInput.safeParse(req.body);
  if (!parsed.success) {
    return res.status(400).json({ ok: false, error: "invalid_input" });
  }
 
  // À partir d'ici, le type CreateOrderInput PORTE l'invariant.
  // Aucune re-validation nécessaire dans createOrder, enqueueFulfillment, etc.
  await createOrder(parsed.data);
  res.status(201).json({ ok: true });
});

Cette approche, théorisée par Alexis King (2019), est devenue standard dans les codebases TypeScript, Rust et Go modernes. Elle supprime la catégorie entière des bugs de validation manquée à mi-pipeline.

3. Encoder selon le contexte de sortie

L'encodage HTML n'est PAS universel. Une donnée utilisateur injectée dans du JS inline, dans un attribut HTML, dans une URL ou dans du CSS nécessite 4 encodages différents. Aligné OWASP Cheat Sheet: Cross Site Scripting Prevention et ASVS V5.3.

Contexte de sortieEncodage correctPiège
Texte HTML (<div>{x}</div>)htmlEncode : &, <, >, ", 'Suffit ici seulement
Attribut HTML (<a title="{x}">)htmlAttributeEncode, guillemets obligatoiresonclick="..." interdit d'y injecter
JS inline (<script>var x = "{x}";</script>)JSON.stringify (échappe \, ", </script>)htmlEncode SEUL est faux
URL parameter (?q={x})encodeURIComponent / urllib.parse.quotehtmlEncode SEUL est faux
CSS value (background: url({x}))CSS.escape ou valeur restreinteSouvent non supporté, préférer rejeter

Exemple TypeScript — ne jamais faire ceci

// ❌ XSS via attribut JS : htmlEncode ne protège pas ici
res.send(`<button onclick="show('${htmlEncode(userInput)}')">Click</button>`);
 
// ✅ Éviter complètement le JS inline, ou utiliser JSON.stringify
const safeJson = JSON.stringify(userInput).replace(/</g, "\\u003c");
res.send(`
  <button id="btn">Click</button>
  <script>
    const payload = ${safeJson};
    document.getElementById("btn").onclick = () => show(payload);
  </script>
`);

4. Comparer les secrets en temps constant

Les comparaisons de tokens, HMAC, API keys ou mots de passe hashés ne doivent jamais utiliser ==, ===, strcmp, Arrays.equals. Ces opérations court-circuitent au premier caractère différent, exposant la longueur du préfixe correct via le timing réseau. CWE-208 (Observable Timing Discrepancy), violation détectée dans ~50 % des codebases auditées 2024 (observations ESN cyber + rapports bug bounty HackerOne 2023-2024).

Exemple multi-langages

# Python
import hmac
def verify_token(provided: bytes, expected: bytes) -> bool:
    return hmac.compare_digest(provided, expected)
// Node.js
import { timingSafeEqual } from "crypto";
function verifyToken(provided: Buffer, expected: Buffer): boolean {
  if (provided.length !== expected.length) return false;
  return timingSafeEqual(provided, expected);
}
// Java
import java.security.MessageDigest;
public static boolean verifyToken(byte[] provided, byte[] expected) {
    return MessageDigest.isEqual(provided, expected);
}
// Go
import "crypto/subtle"
func VerifyToken(provided, expected []byte) bool {
    return subtle.ConstantTimeCompare(provided, expected) == 1
}

5. Autorisation côté serveur non-négociable

Un ID ressource reçu du client n'implique jamais d'autorisation. Le serveur doit systématiquement vérifier : « cet utilisateur est-il autorisé à accéder à cette ressource ? ». OWASP Top 10 A01:2021 Broken Access Control, CWE-285, CWE-639 (IDOR). C'est la classe de vulnérabilité la plus fréquente en bug bounty 2024 (source : HackerOne Hacker-Powered Security Report 2024, 33 % des critical bounties).

Exemple Python / FastAPI

# ❌ IDOR classique : on fait confiance à l'ID d'URL
@router.get("/invoices/{invoice_id}")
async def get_invoice(invoice_id: UUID, user: User = Depends(current_user)):
    invoice = await Invoice.get(invoice_id)
    if not invoice:
        raise HTTPException(404)
    return invoice  # ❌ tout utilisateur authentifié voit toute facture
 
# ✅ Vérification d'ownership / authorization explicite
@router.get("/invoices/{invoice_id}")
async def get_invoice(invoice_id: UUID, user: User = Depends(current_user)):
    invoice = await Invoice.get(invoice_id)
    if not invoice:
        raise HTTPException(404)
 
    # Autorisation centralisée, pas dispersée dans chaque route
    if not can_user_read_invoice(user, invoice):
        # Retourner 404 plutôt que 403 pour ne pas divulguer l'existence
        raise HTTPException(404)
 
    return invoice

Règle structurante

Externaliser les décisions d'autorisation dans une policy layer testable (OPA/Rego, oso, Casbin) ou un module applicatif isolé. Jamais de règle d'autorisation inline dispersée sur des centaines de routes — coût de review explosant, couverture tests nulle.

6. Cryptographie haute-niveau, jamais roll your own

N'implémentez jamais d'AES, de CBC, de padding, de HMAC à la main. Utilisez des APIs haute-niveau qui gèrent mode + nonce + auth ensemble. Aligné ASVS V6.2 et NIST SP 800-175B.

Choix à éviterRemplacement recommandé 2025
AES-CBC + HMAC manuelAES-GCM ou ChaCha20-Poly1305 (AEAD)
MD5, SHA-1 (cassés)SHA-256 minimum, SHA-3 pour nouveaux designs
MD5/SHA-256 pour mots de passeargon2id (défaut 2025), sinon bcrypt/scrypt
Math.random(), rand()Sources crypto : secrets (Python), crypto.randomBytes (Node), crypto/rand (Go)
RSA < 3072 bitsRSA 3072+ minimum, Ed25519 préférable
TLS 1.0/1.1TLS 1.2 minimum, TLS 1.3 cible

Exemple Python : chiffrement AEAD avec PyNaCl (libsodium)

from nacl.secret import SecretBox
from nacl.utils import random
 
# Clé 32 octets, stockée dans secret manager (KMS, Vault)
key = random(SecretBox.KEY_SIZE)  # en prod : load_from_vault("app-encryption-key")
box = SecretBox(key)
 
# Chiffrement : nonce automatique unique, auth tag intégré
ciphertext = box.encrypt(b"donnee sensible")
 
# Déchiffrement : vérification auth automatique, lève CryptoError si tampered
plaintext = box.decrypt(ciphertext)

Pas de nonce manuel à générer, pas de HMAC à coller, pas de padding à gérer. L'API fait ce qu'il faut, ce qui élimine 90 % des bugs crypto applicatifs observés en pentest.

7. Éviter les TOCTOU (Time-Of-Check / Time-Of-Use)

Vérifier une condition puis l'utiliser plus tard expose à une race condition si l'état change entre les deux. CWE-367. Classe de bug sous-diagnostiquée car difficilement détectable par les scans statiques traditionnels.

Contre-exemple Go — vérification séparée de l'écriture

// ❌ TOCTOU : fichier peut être remplacé par un symlink entre Lstat et Open
info, err := os.Lstat(path)
if err != nil { return err }
if info.Mode().IsRegular() {
    f, err := os.Open(path)  // ❌ peut ouvrir un symlink créé entre-temps
    // ...
}

Version correcte — opération atomique

// ✅ Open avec O_NOFOLLOW : refuse les symlinks atomiquement
f, err := os.OpenFile(path, os.O_RDONLY | syscall.O_NOFOLLOW, 0)
if err != nil { return err }
defer f.Close()
// On vérifie le type APRÈS ouverture, sur le fd, pas sur le path
info, err := f.Stat()
if err != nil { return err }
if !info.Mode().IsRegular() {
    return errors.New("not a regular file")
}

TOCTOU métier : le double-spend

# ❌ Race condition sur le solde : deux requêtes simultanées passent la vérif
def transfer(account_id: UUID, amount: Decimal):
    balance = Account.get(account_id).balance
    if balance < amount:
        raise InsufficientFunds()
    Account.debit(account_id, amount)  # deux requêtes peuvent débiter
 
# ✅ Décrément conditionnel atomique en base, UPDATE ... WHERE balance >= amount
def transfer(account_id: UUID, amount: Decimal):
    updated = db.execute(
        "UPDATE accounts SET balance = balance - :amt "
        "WHERE id = :id AND balance >= :amt",
        {"id": account_id, "amt": amount}
    )
    if updated.rowcount == 0:
        raise InsufficientFunds()

8. Désérialisation sûre

La désérialisation de formats codant du comportement (Python pickle, Java ObjectInputStream, Ruby Marshal, PHP unserialize, .NET BinaryFormatter) peut conduire à une RCE si le contenu est sous contrôle attaquant. OWASP Top 10 A08:2021 Software and Data Integrity Failures, CWE-502. Classe responsable de CVE critiques récurrentes (Log4Shell CVE-2021-44228 de 2021 en est un descendant, Spring4Shell CVE-2022-22965 en 2022, exploitations Java persistantes 2023-2024).

Règles pratiques

FormatRègle
Python pickle / Ruby Marshal / PHP unserializeNe jamais désérialiser de donnée non-trusted. Utiliser JSON ou CBOR.
Java ObjectInputStreamDéprécier. Utiliser Jackson JSON + validation Bean. Si inévitable : ObjectInputFilter JEP-290.
.NET BinaryFormatterDéprécié Microsoft depuis .NET 5. Utiliser System.Text.Json.
YAMLyaml.safe_load() en Python, jamais yaml.load nu.
XMLDésactiver entités externes (XXE) et DTD : etree.parse(..., parser=XMLParser(resolve_entities=False)).

Exemple Python safe YAML

import yaml
 
# ❌ yaml.load accepte des constructeurs Python arbitraires → RCE
config = yaml.load(user_upload, Loader=yaml.Loader)  # NE JAMAIS FAIRE
 
# ✅ safe_load interdit les types non-primitifs
config = yaml.safe_load(user_upload)

9. SSRF en allowlist, pas denylist

Les denylists d'IP privées (169.254.0.0/16 metadata, 10.0.0.0/8, 127.0.0.0/8) sont systématiquement contournables : DNS rebinding, redirections HTTP multi-hop, IPv6 équivalents, encodages URL variants (127.1, 0177.0.0.1, 2130706433 décimal), IPs publiques cloud pointant en interne. OWASP Top 10 A10:2021 SSRF, CWE-918.

Pattern robuste TypeScript

import dns from "node:dns/promises";
import { URL } from "node:url";
import net from "node:net";
 
const ALLOWED_HOSTS = new Set([
  "api.partner1.com",
  "webhook.partner2.io",
]);
 
async function safeFetch(rawUrl: string): Promise<Response> {
  const url = new URL(rawUrl);
 
  // 1. Allowlist stricte de host
  if (!ALLOWED_HOSTS.has(url.hostname)) {
    throw new Error("host_not_allowed");
  }
 
  // 2. Scheme restreint
  if (url.protocol !== "https:") {
    throw new Error("scheme_not_allowed");
  }
 
  // 3. Résolution DNS côté serveur + validation de l'IP finale
  const { address } = await dns.lookup(url.hostname);
  if (isPrivateIp(address)) {
    throw new Error("resolved_to_private_ip");
  }
 
  // 4. Fetch avec redirections désactivées
  return fetch(url, { redirect: "error" });
}
 
function isPrivateIp(ip: string): boolean {
  if (net.isIPv4(ip)) {
    const [a, b] = ip.split(".").map(Number);
    return (
      a === 10 ||
      (a === 172 && b >= 16 && b <= 31) ||
      (a === 192 && b === 168) ||
      a === 127 ||
      (a === 169 && b === 254) ||
      a === 0
    );
  }
  if (net.isIPv6(ip)) {
    return ip.startsWith("::1") || ip.startsWith("fc") || ip.startsWith("fd") || ip.startsWith("fe80");
  }
  return false;
}

La protection robuste combine allowlist hostname + scheme fixé + résolution DNS serveur + validation IP finale + interdiction de redirections. Un seul de ces contrôles pris isolément est contournable.

10. Logs sécurisés — jamais de secrets ni de PII brute

Les logs sont souvent indexés (SIEM, stack ELK, Loki) et consultés par des équipes larges. Logguer des secrets (tokens, mots de passe, clés), des PII (numéro CNI, carte bancaire, email brut sur certains contextes RGPD) ou des données de santé crée des fuites indirectes. CWE-532, violation détectée régulièrement lors des audits post-incident.

Pattern Python : redaction automatique par filtre

import logging
import re
 
SECRETS_PATTERNS = [
    re.compile(r"(?i)(authorization|cookie|password|token|api[_-]?key)\s*[=:]\s*\S+"),
    re.compile(r"Bearer\s+[A-Za-z0-9._-]+"),
    re.compile(r"\b\d{13,19}\b"),  # PAN carte bancaire naïf
]
 
class RedactFilter(logging.Filter):
    def filter(self, record: logging.LogRecord) -> bool:
        msg = record.getMessage()
        for pattern in SECRETS_PATTERNS:
            msg = pattern.sub("[REDACTED]", msg)
        record.msg = msg
        record.args = ()
        return True
 
logger = logging.getLogger("app")
logger.addFilter(RedactFilter())
 
# ✅ Pour les identifiants utilisateurs, préférer un hash / surrogate ID
logger.info("user_login", extra={
    "user_id_hash": sha256(user.email.encode()).hexdigest()[:12],
    "ip": anonymize_ip(request.remote_addr),
})

Règles opérationnelles

  1. Jamais de header Authorization, Cookie, X-API-Key en log brut.
  2. Jamais de body de requête si non échantillonné et non filtré.
  3. Jamais de stack trace renvoyée au client (fuite d'architecture + CWE-209).
  4. Identifiants utilisateurs loggués en forme pseudonymisée si les logs sortent du périmètre régulé (RGPD article 4).

11. Prévenir la prototype pollution (écosystème JS / Node.js)

La prototype pollution est une classe spécifique à JavaScript où un attaquant modifie Object.prototype via une clé __proto__ ou constructor.prototype, affectant tous les objets créés après. CWE-1321. CVE fréquentes 2022-2024 sur lodash, minimist, merge, hoek, set-value.

Contre-exemple et fix

// ❌ Merge naïf : copie la clé __proto__
function merge(target: any, source: any): any {
  for (const key in source) {
    if (typeof source[key] === "object") {
      target[key] = merge(target[key] || {}, source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}
 
// Entrée malicieuse
merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));
console.log(({} as any).isAdmin); // true → pollution globale
 
// ✅ Fix : filtrer les clés dangereuses + Object.hasOwn
const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
 
function safeMerge(target: Record<string, unknown>, source: Record<string, unknown>) {
  for (const key of Object.keys(source)) {
    if (FORBIDDEN_KEYS.has(key)) continue;
    if (!Object.hasOwn(source, key)) continue;
    const value = source[key];
    if (value && typeof value === "object" && !Array.isArray(value)) {
      target[key] = safeMerge(
        (target[key] as Record<string, unknown>) ?? Object.create(null),
        value as Record<string, unknown>
      );
    } else {
      target[key] = value;
    }
  }
  return target;
}

Alternative structurante : préférer Map pour les dictionnaires de clés dynamiques (new Map() n'a pas de prototype exploitable), ou Object.create(null) pour des objets sans prototype.

12. Mass assignment : verrouiller les champs modifiables

Les ORM et frameworks modernes permettent de construire des objets depuis un body JSON d'un coup. Si tous les champs sont assignables, un attaquant peut injecter {"is_admin": true} dans un PATCH /profile. OWASP API Top 10 2023 API6, CWE-915. Classe de bug majeure sur Ruby on Rails historiquement (CVE GitHub 2012), persistante sur Django, Spring, TypeORM.

Exemple TypeScript / TypeORM

// ❌ Mass assignment : tous les champs body écrasent l'entity
app.patch("/profile", async (req, res) => {
  const user = await userRepo.findOneByOrFail({ id: req.user.id });
  Object.assign(user, req.body);  // ❌ body contient "role": "admin"
  await userRepo.save(user);
  res.json({ ok: true });
});
 
// ✅ Allowlist explicite des champs modifiables
const UpdateProfileInput = z.object({
  displayName: z.string().min(1).max(80),
  avatarUrl: z.string().url().optional(),
  bio: z.string().max(500).optional(),
});
 
app.patch("/profile", async (req, res) => {
  const input = UpdateProfileInput.safeParse(req.body);
  if (!input.success) return res.status(400).json({ ok: false });
 
  await userRepo.update(
    { id: req.user.id },
    input.data  // ✅ seuls les 3 champs whitelisted
  );
  res.json({ ok: true });
});

Règle générale

Définir des DTOs d'entrée distincts des entités. L'entité base de données contient role, tenant_id, created_at ; le DTO PATCH profil ne contient que les 3-4 champs utilisateur-modifiables. Zod, class-validator, Pydantic, Bean Validation assurent le filtrage sans code manuel.

Points clés à retenir

  • Fail Closed : refuser par défaut sur erreur, jamais laisser passer « parce que la policy n'a pas chargé ».
  • Trust Boundary : valider une fois à l'entrée avec types qui portent l'invariant (parse-don't-validate), pas re-valider à chaque fonction.
  • Context-aware encoding : htmlEncode, JSON.stringify, encodeURIComponent, CSS.escape — quatre contextes, quatre encodages.
  • Timing-safe compare : jamais == sur un token, HMAC ou API key — hmac.compare_digest, timingSafeEqual, MessageDigest.isEqual, subtle.ConstantTimeCompare.
  • Server-side authorization : politique centralisée, 404 plutôt que 403, ID URL jamais trusté.
  • Crypto haute-niveau : AES-GCM / ChaCha20-Poly1305 AEAD, argon2id pour mots de passe, Ed25519 pour signatures, sources aléatoires crypto.
  • TOCTOU : atomicité via APIs système (O_NOFOLLOW) ou UPDATE conditionnel en base.
  • Désérialisation : jamais de pickle / ObjectInputStream / BinaryFormatter sur input non-trusté ; JSON + schéma sinon.
  • SSRF : allowlist hostname + scheme + résolution DNS serveur + IP finale vérifiée + redirections interdites.
  • Logs : filtre de redaction systématique, pseudonymisation des user IDs, jamais de stack trace au client.
  • Prototype pollution : filtrer __proto__ / constructor / prototype, préférer Map ou Object.create(null).
  • Mass assignment : DTOs d'entrée et de sortie distincts de l'entité, allowlist explicite.

Pour consolider la pratique, voir Roadmap secure coding (parcours progressif 4 phases) et Roadmap AppSec Engineer. Pour le pendant validation offensive, OWASP Testing Guide expliqué et Méthodologie pentest web.

Questions fréquentes

  • Pourquoi commencer par 12 principes plutôt que par une liste de vulnérabilités ?
    Parce qu'une liste de vulnérabilités (OWASP Top 10, CWE Top 25) est un catalogue d'effets observés, pas un guide d'action. Elle donne les symptômes à traiter a posteriori. Les principes de secure coding sont les causes racines : appliquer Fail Closed, Trust Boundary Validation, Server-Side Authorization et Context-Aware Output Encoding prévient mécaniquement 80 à 90 % des vulnérabilités OWASP Top 10 sans les énumérer. Les équipes dev qui apprennent d'abord les CWE produisent du code qui corrige les erreurs connues puis introduit des variantes non cataloguées. Les équipes qui apprennent les principes produisent du code qui résiste aux classes d'attaque, y compris celles qui n'existent pas encore au moment de l'écriture.
  • Le principe le plus souvent violé par les développeurs expérimentés ?
    La comparaison non timing-safe de secrets (tokens, HMAC, API keys). L'écrasante majorité des développeurs, même seniors, comparent des tokens avec == ou strcmp. Cette comparaison court-circuite au premier caractère différent, divulguant la longueur du préfixe correct via le timing de réponse sur plusieurs milliers de requêtes. Les attaques timing sur le web sont réalisables quand le bruit réseau est stabilisé par répétition massive (1000+ mesures). La fix est triviale : hmac.compare_digest en Python, crypto.timingSafeEqual en Node.js, MessageDigest.isEqual en Java, subtle.ConstantTimeCompare en Go. Pourtant, on la trouve violée dans environ 50 % des codebases auditées 2024.
  • Faut-il valider les inputs partout ou seulement aux frontières ?
    Aux frontières uniquement, en input. Valider partout produit du code pollué de contrôles redondants qui masquent les vraies invariants et gonflent la complexité. La règle correcte suit le modèle des Trust Boundaries OWASP : valider et normaliser dès qu'une donnée entre dans le périmètre de confiance (HTTP handler, message queue consumer, import CSV), puis propager des types primitives ou domaines qui portent l'invariant de validation. Par exemple un Email validé à l'entrée devient un type Email interne ; les fonctions qui le reçoivent n'ont pas besoin de re-valider. Cette discipline, alignée NIST SP 800-53 SI-10 et OWASP ASVS V5, est la base du parse-don't-validate popularisé par Alexis King (2019) et adopté massivement en TypeScript et Rust modernes.
  • Les ORM protègent-ils automatiquement contre l'injection SQL ?
    Partiellement. Les ORM modernes (SQLAlchemy, Prisma, TypeORM, Sequelize, Hibernate) paramètrent les requêtes générées et protègent efficacement contre l'injection SQL classique. Mais trois vecteurs restent ouverts : 1) Les requêtes raw ou SQL brut (execute, raw_query) où le développeur ré-introduit la concaténation. 2) Le mass assignment non verrouillé — l'ORM accepte des champs non prévus dans la requête, ex: permettre la mise à jour du champ is_admin via un formulaire profil utilisateur. 3) Les expressions ORDER BY et colonnes dynamiques souvent non paramétrables et qui reviennent à la concaténation. Un code review AppSec regarde ces trois patterns en priorité, indépendamment de la présence d'un ORM.
  • Le SSRF peut-il être prévenu efficacement avec une denylist d'IP privées ?
    Non, pas de manière fiable. Les denylists SSRF (bloquer 169.254.0.0/16 pour les metadata, 10.0.0.0/8, 127.0.0.0/8) sont systématiquement contournables par DNS rebinding, redirections multi-hop, adresses IPv6 équivalentes, encodages URL variants (127.1, 0177.0.0.1, 2130706433), et services cloud avec IPs publiques pointant en interne. La défense robuste est une allowlist : liste stricte des hôtes ou préfixes autorisés, résolution DNS contrôlée côté serveur, validation post-résolution de l'IP finale. C'est la recommandation SPIP OWASP Cheat Sheet et NIST SP 800-204 depuis 2021. Le shift denylist→allowlist est une des transformations à plus fort ROI sécurité dans un codebase mature.
  • Les principes changent-ils entre langages (Python, Java, Go, TypeScript) ?
    Les principes sont identiques — la sécurité est indépendante du langage. Les mécanismes d'application diffèrent : libsodium/PyNaCl en Python vs crypto natif Node.js vs javax.crypto en Java ; types sum algébriques en TypeScript 5+ vs sealed classes Java 17+ vs enums tagués Go 1.21+ ; tests de propriété Hypothesis Python vs fast-check TypeScript. L'écart de maturité se mesure plutôt sur l'écosystème : Rust et Go exposent des APIs sûres par défaut (subtle.ConstantTimeCompare, rand.Read), Java et C# imposent une vigilance accrue sur les APIs legacy (ObjectInputStream, BinaryFormatter). Python et Ruby sont les plus permissifs : principes identiques, application plus rigoureuse requise.

Écrit par

Naim Aouaichia

Expert cybersécurité et fondateur de Zeroday Cyber Academy

Expert cybersécurité avec un master spécialisé et un parcours hybride : développement, DevOps, DevSecOps, SOC, GRC. Fondateur de Hash24Security et Zeroday Cyber Academy. Formateur et créateur de contenu technique sur la cybersécurité appliquée, la sécurité des LLM et le DevSecOps.