Sécurité d’abord

Prévention de l’injection SQL

Les valeurs fournies par l’utilisateur ne sont jamais interpolées dans le texte SQL — elles sont liées via des placeholders de paramètres (spécifiques au dialecte, ex. $1 ou ?)

Liées par paramètres
100%
Toutes les valeurs fournies par l’utilisateur utilisent des placeholders
Interpolation de valeurs
Zéro
Les données utilisateur n’apparaissent jamais dans le texte SQL
Préservation de l’ordre
Garanti
Les placeholders correspondent aux params 1:1
Tests de sécurité
Actuelle
Couverture des tentatives d’injection

Comprendre l’injection SQL

L’injection SQL reste l’une des vulnérabilités les plus critiques des applications web

Une injection SQL survient quand des données non fiables sont incorporées au texte SQL sans liaison de paramètres sûre. Des attaquants peuvent manipuler le sens de la requête pour accéder à des données non autorisées, modifier des enregistrements ou déclencher des opérations non désirées.

Schémas d’attaque courants

Contournement d’authentification

admin' OR '1'='1

Tente de contourner la connexion en rendant la condition WHERE toujours vraie

Exfiltration de données

' UNION SELECT password FROM users--

Utilise UNION pour tenter d’extraire des données d’autres tables

Commandes destructrices

'; DROP TABLE users; --

Tente d’ajouter des instructions supplémentaires pour supprimer des données

Abus de bug d’ordre des paramètres

Exploit : si les placeholders et les params ne correspondent pas, les contrôles d’autorisation peuvent utiliser les mauvaises valeurs

Exemple : WHERE userId=$1 AND isAdmin=$2, mais params=[true, 123] au lieu de [123, true] peut accorder un accès admin à tort

Stratégie de protection multi-couches

Chaque requête passe par plusieurs couches de validation de sécurité

  1. Couche 1 : Liaison automatique des paramètres

    Toutes les valeurs fournies par l’utilisateur sont converties en paramètres liés. Les valeurs n’entrent pas dans le texte SQL comme littéraux.

    • Les valeurs utilisateur sont représentées par des placeholders spécifiques au dialecte (ex. $1 ou ?)
    • Les entrées tableau sont paramétrées (placeholders étendus ou paramètres array selon le dialecte/la stratégie)
    • NULL et valeurs optionnelles gérés sans concaténer l’entrée utilisateur au texte SQL
    • Les valeurs date/heure sont paramétrées
    • Les valeurs JSON sont paramétrées
  2. Couche 2 : Validation des noms de champs

    Chaque nom de champ est validé contre les métadonnées du schéma Prisma avant la génération de la requête.

    • Seuls les champs définis dans le schéma sont autorisés
    • Filtres de relation validés via les métadonnées du schéma
    • Aucun nom de champ arbitraire accepté
    • Champs non pris en charge/calculés rejetés le cas échéant
    • Les payloads de prototype pollution sont rejetés (ex. __proto__, constructor)
  3. Couche 3 : Assainissement des identifiants

    Les noms de tables, de colonnes et les alias sont validés et quotés/échappés de manière sûre si nécessaire.

    • Caractères de contrôle rejetés
    • Mots réservés quotés si nécessaire
    • Qualification de schéma prise en charge si configurée
    • Échappement des guillemets doubles (ou équivalent du dialecte) pour les identifiants
    • Longueur maximale d’identifiant appliquée par dialecte
  4. Couche 4 : Validation des opérateurs

    Seuls des opérateurs connus et sûrs sont autorisés pour chaque type de champ.

    • Opérateurs de chaîne : contains, startsWith, endsWith
    • Opérateurs numériques : lt, lte, gt, gte
    • Opérateurs de tableau : in, notIn avec validation de type
    • Opérateurs logiques : AND, OR, NOT validés structurellement
    • Opérateurs inconnus rejetés immédiatement
  5. Couche 5 : Garantie d’ordre des paramètres

    Un ordre strict garantit que chaque placeholder correspond à exactement un paramètre dans l’ordre d’insertion, ce qui empêche les décalages.

    • Un compteur séquentiel empêche le réordonnancement
    • Construction du SQL et du tableau params en synchronisation
    • Aucun buffer intermédiaire ni opération de réordonnancement
    • Vérification par les tests : SQL généré et params toujours alignés
    • Correspondance un-à-un : placeholder N → params[N-1] (ou correspondance équivalente selon le dialecte)

Garanties formelles de sécurité

Esquisses de preuve de résistance à l’injection sous les hypothèses de conception énoncées (liaison de paramètres, aucun contournement par SQL brut, usage correct du driver)

Théorème 1 : Isolation des valeurs

Pour toute valeur V fournie par l’utilisateur, il n’existe aucun chemin d’exécution où V apparaît comme texte SQL dans la chaîne de requête générée.

Par construction, tous les chemins de code qui traitent des valeurs utilisateur appellent addParameter(value), qui : 1. Stocke la valeur dans un tableau params séparé 2. Renvoie un jeton placeholder pour le texte SQL 3. Seul le jeton placeholder est ajouté à la chaîne SQL 4. Le driver de base de données reçoit séparément le texte SQL et les valeurs des paramètres ∴ Les valeurs utilisateur ne sont pas analysées comme de la syntaxe SQL

const addParameter = (params: any[], value: any) => {
  params.push(value)
  const index = params.length
  return `$${index}`
}

Théorème 2 : Fermeture des noms de champs

L’ensemble des noms de champs F de toute requête générée est un sous-ensemble des champs S définis dans le schéma, c.-à-d. F ⊆ S.

Pour chaque référence de champ : 1. Le nom du champ est extrait de l’objet de requête 2. Une recherche est effectuée dans les métadonnées du schéma 3. Si le champ n’existe pas dans le schéma, une erreur est levée 4. Seuls les champs validés atteignent la génération SQL ∴ Des noms de champs arbitraires ne peuvent pas apparaître dans le SQL

const validateField = (field: string, model: Model) => {
  if (!model.fields.has(field)) {
    throw new Error(`Field ${field} does not exist`)
  }
  return model.fields.get(field)
}

Théorème 3 : Sécurité des opérateurs

Pour tout opérateur O appliqué au champ F, O appartient aux opérateurs autorisés pour le type T de F.

Algorithme de validation des opérateurs : 1. Extraire le type de champ T du schéma 2. Définir allowed_ops(T) = { opérateurs valides pour le type T } 3. Pour l’opérateur O de la requête : - Si O ∉ allowed_ops(T), lever une erreur - Sinon, appliquer l’opérateur avec liaison de paramètres ∴ Seuls des opérateurs adaptés au type peuvent être utilisés

const ALLOWED_OPS: Record<string, string[]> = {
  String: ['contains', 'startsWith', 'endsWith'],
  Int: ['lt', 'lte', 'gt', 'gte']
}
if (!ALLOWED_OPS[fieldType]?.includes(operator)) {
  throw new Error('Invalid operator')
}

Théorème 4 : Sécurité des identifiants

Tous les identifiants SQL I sont validés pour exclure les caractères de contrôle et garantir une quotation/un échappement sûrs selon le dialecte.

Traitement des identifiants : 1. Rejet s’il contient des caractères de contrôle 2. Échappement des guillemets internes selon les règles du dialecte 3. Quotation des identifiants si nécessaire (mots réservés ou caractères spéciaux) 4. Application des limites de longueur d’identifiant propres au dialecte ∴ Les identifiants ne peuvent ni casser la syntaxe SQL ni introduire de jetons injectés

const quoteIdentifier = (id: string) => {
  if (/[\x00-\x1F]/.test(id)) {
    throw new Error('Invalid characters')
  }
  const escaped = id.replace(/"/g, '""')
  const needsQuoting = /[^a-z0-9_]/i.test(id) || isReservedKeyword(id)
  return needsQuoting ? `"${escaped}"` : id
}

Théorème 5 : Cohérence de l’ordre des paramètres

Pour chaque position de placeholder N émise dans le texte SQL, params[N-1] contient exactement la valeur prévue pour cette position, sans réordonnancement ni décalage.

Garantie d’ordre des paramètres : 1. Un tracker unique conserve l’ordre d’insertion 2. Chaque appel à add() ajoute la valeur à params et émet immédiatement le jeton placeholder suivant 3. La construction du texte SQL et celle de params avancent en synchronisation 4. Aucune opération de réordonnancement intermédiaire n’a lieu ∴ La correspondance un-à-un est maintenue de bout en bout

class ParameterTracker {
  private params: any[] = []

  add(value: any): string {
    this.params.push(value)
    return `$${this.params.length}`
  }

  getParams(): any[] {
    return this.params
  }
}

const tracker = new ParameterTracker()
const sql = `WHERE email = ${tracker.add(email)} AND age > ${tracker.add(age)}`
const params = tracker.getParams()

Couverture de tests de sécurité complète

La suite actuelle valide la protection contre les vecteurs d’injection SQL courants et les cas limites

Paramétrisation des valeurs (basic.test.ts)

  • Chaînes avec apostrophes : user'with'quotes
  • Chaînes avec points-virgules : user;extra
  • Mots-clés SQL comme valeurs : DROP TABLE users
  • Injection complexe : '; DROP TABLE users; --
  • Attaques UNION : ' UNION SELECT * FROM users--
  • Attaques booléennes : admin' OR '1'='1
  • Injection de commentaire : test@example.com' -- comment

Validation des noms de champs (identifiers.test.ts)

  • Rejet des noms de champs malveillants dans SELECT
  • Rejet des noms de champs malveillants dans WHERE
  • Rejet des noms de champs malveillants dans ORDER BY
  • Rejet de l’injection SQL dans les noms de champs
  • Rejet des champs inexistants
  • Rejet de la prototype pollution : __proto__, constructor

Sécurité des patterns LIKE (like-patterns.test.ts)

  • Injection de wildcard : %' OR '1'='1
  • Injection d’underscore : test_' OR '1'='1
  • Gestion des backslashs : test\\'; DROP--
  • Wildcards multiples : %_%'; DROP--
  • Injection insensible à la casse : '; UNION SELECT--

Sécurité des opérateurs de tableau (array-operators.test.ts)

  • IN avec tableaux malveillants : ['; DROP--', 'UNION SELECT--']
  • NOT IN avec injection : ['; TRUNCATE--', 'DELETE FROM--']
  • Gestion des tableaux vides
  • Validation de grands tableaux (100+ éléments)
  • Gestion des tableaux de types mixtes

Cas limites (edge-cases.test.ts)

  • Injection Unicode : \u0027 OR \u00271\u0027=\u00271
  • Injection encodée en hexadécimal : 0x31=0x31--
  • Encodage URL : %27%3B%20DROP%20TABLE
  • Requêtes empilées : '; DROP TABLE users; SELECT
  • Injection aveugle temporelle (selon le dialecte) : WAITFOR DELAY '00:00:05'--
  • Basée sur UNION : UNION ALL SELECT null, password
  • Tentatives d’injection de second ordre

Vérification de l’ordre des paramètres (basic.test.ts)

  • Positions de placeholders séquentielles
  • Le tableau de paramètres suit l’ordre des placeholders
  • Les requêtes complexes conservent l’ordre entre les conditions
  • Les conditions OR/AND imbriquées préservent la séquence des paramètres
  • Les filtres de relation conservent une correspondance correcte des paramètres

Sûr vs dangereux : comparaison de code

❌ Dangereux : concaténation de chaînes
unsafe.ts
const email = req.body.email
const sql = "SELECT * FROM users WHERE email = '" + email + "'"
✅ Sûr : liaison automatique des paramètres
safe.ts
const email = req.body.email
const { sql, params } = toSQL('User', 'findMany', {
  where: { email }
})

const result = { sql, params }

Détails d’implémentation

Gestion des paramètres

Un suivi centralisé des paramètres garantit que chaque valeur fournie par l’utilisateur est liée par paramètre et que son ordre reste stable

class ParameterTracker {
  private params: any[] = []

  add(value: any): string {
    this.params.push(value)
    return `$${this.params.length}`
  }

  getParams(): any[] {
    return this.params
  }
}

Validation des champs

La validation basée sur le schéma empêche l’accès arbitraire aux champs

function validateField(
  fieldName: string,
  model: ModelInfo
): FieldInfo {
  const field = model.fields.get(fieldName)
  if (!field) {
    throw new Error(
      `Field "${fieldName}" does not exist`
    )
  }
  return field
}

Quotation des identifiants

Une validation et une quotation des identifiants adaptées au dialecte empêchent l’injection de syntaxe via les identifiants

function quoteIdentifier(identifier: string): string {
  if (/[\x00-\x1F]/.test(identifier)) {
    throw new Error('Invalid control characters')
  }

  const escaped = identifier.replace(/"/g, '""')
  const needsQuoting = /[^a-z0-9_]/i.test(identifier) || isReservedKeyword(identifier)

  if (needsQuoting) {
    return `"${escaped}"`
  }

  return identifier
}

Bonnes pratiques de sécurité

Ne jamais désactiver la validation

Ne contournez pas la validation du schéma ni les vérifications de champs. Ce sont des couches de sécurité critiques.

À faire
Utiliser la bibliothèque comme prévu, avec la validation complète
À éviter
Contourner la validation du schéma ou injecter du SQL brut dans le texte de requête

Garder le schéma à jour

Assurez-vous que votre schéma Prisma reflète fidèlement la structure de votre base de données.

À faire
Exécuter prisma db pull et prisma generate après des changements de schéma
À éviter
Utiliser un DMMF ou des définitions de schéma obsolètes

Valider les types d’entrée

TypeScript apporte la sécurité à la compilation, mais la validation au runtime ajoute une défense en profondeur.

À faire
Utiliser les types générés par Prisma pour les arguments de requête
À éviter
Caster l’entrée utilisateur en any avant de la passer aux requêtes

Surveiller les patterns de requêtes

Loggez et surveillez le SQL généré et les paramètres en production pour détecter les anomalies et les abus.

À faire
Activer le logging des requêtes et analyser les patterns
À éviter
Exécuter en production sans monitoring

La sécurité par conception

La protection contre l’injection SQL est intégrée à chaque couche de cette bibliothèque. Consultez directement les tests de sécurité et les détails d’implémentation.