🔒 Sécurité d’abord

Injection SQL Prévention

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 ?)

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

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 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: If placeholders and params are mismatched, authorization checks may use the wrong values

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é

Couche 1 : Liaison automatique des paramètres

Toutes les valeurs utilisateur sont converties en paramètres liés. Elles 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 dialecte/stratégie)
  • NULL et valeurs optionnelles gérés sans concaténer l’entrée utilisateur au texte SQL
  • Valeurs date/heure paramétrées
  • Valeurs JSON paramétrées

Couche 2 : Validation des noms de champs

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

  • Seuls les champs du schéma sont autorisés
  • Filtres de relation validés via métadonnées
  • Aucun nom de champ arbitraire accepté
  • Champs non supportés/calculés rejetés si nécessaire
  • Payloads de prototype pollution rejetés (ex. __proto__, constructor)

Couche 3 : Assainissement des identifiants

Noms de tables, colonnes et alias validés et correctement quotés/échappés.

  • Caractères de contrôle rejetés
  • Mots réservés quotés si nécessaire
  • Qualification de schéma supportée si configurée
  • Échappement des guillemets (ou équivalent)
  • Longueur max d’identifiant appliquée par dialecte

Couche 4 : Validation des opérateurs

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

  • Opérateurs string : contains, startsWith, endsWith
  • Opérateurs numériques : lt, lte, gt, gte
  • Opérateurs array : in, notIn avec validation de type
  • Opérateurs logiques : AND, OR, NOT validés structurellement
  • Opérateurs inconnus rejetés immédiatement

Couche 5 : Garantie d’ordre des paramètres

Un ordre strict assure qu’un placeholder correspond à exactement un paramètre dans l’ordre d’insertion.

  • Compteur séquentiel empêche le réordonnancement
  • Construction SQL et tableau params en synchronisation
  • Pas de buffer intermédiaire ni réordonnancement
  • Vérification par tests : SQL et params alignés
  • Correspondance 1–1 : placeholder N → params[N-1] (ou équivalent selon dialecte)

Garanties formelles de sécurité

Esquisses de preuve de résistance à l’injection sous hypothèses (liaison de paramètres, pas de bypass raw SQL, usage correct du driver)

Théorème 1 : Isolation des valeurs

For all user-provided values V, there exists no execution path where V appears as SQL text in the generated query string.

By construction, all code paths that handle user values call addParameter(value) which: 1. Stores the value in a separate params array 2. Returns a placeholder token for the SQL text 3. Only the placeholder token is appended into the SQL string 4. The database driver receives SQL text and parameter values separately ∴ User values are not parsed as SQL syntax

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

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

The set of field names F in any generated query is a subset of schema-defined fields S, i.e., F ⊆ S.

For every field reference: 1. Field name extracted from query object 2. Lookup performed against schema metadata 3. If the field does not exist in the schema, an error is thrown 4. Only validated fields reach SQL generation ∴ Arbitrary field names cannot appear in 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

For any operator O applied to field F, O is a member of the allowed operators for F's type T.

Operator validation algorithm: 1. Extract field type T from schema 2. Define allowed_ops(T) = { valid operators for type T } 3. For operator O in query: - If O ∉ allowed_ops(T), throw error - Else apply operator using parameter binding ∴ Only type-appropriate operators can be used

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

All SQL identifiers I are validated to prevent control characters and to ensure safe quoting/escaping per dialect.

Identifier processing: 1. Reject if contains control characters 2. Escape internal quote characters per dialect rules 3. Quote identifiers when required (reserved keywords or special characters) 4. Enforce per-dialect identifier length limits ∴ Identifiers cannot break SQL syntax or introduce injected tokens

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 d’ordre des paramètres

For every placeholder position N emitted into the SQL text, params[N-1] contains the exact value intended for that position, with no reordering or mismatch.

Parameter ordering guarantee: 1. A single tracker maintains insertion order 2. Each add() call appends the value to params and immediately emits the next placeholder token 3. SQL text construction and params construction proceed in lockstep 4. No intermediate reordering operations occur ∴ One-to-one correspondence is maintained throughout

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

137 tests valident la protection contre les vecteurs SQLi courants et cas limites (suite actuelle)

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

  • Strings with quotes: user'with'quotes
  • Strings with semicolons: user;extra
  • SQL keywords as values: DROP TABLE users
  • Complex injection: '; DROP TABLE users; --
  • Union attacks: ' UNION SELECT * FROM users--
  • Boolean attacks: admin' OR '1'='1
  • Comment injection: test@example.com' -- comment

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

  • Reject malicious field names in SELECT
  • Reject malicious field names in WHERE
  • Reject malicious field names in ORDER BY
  • Reject SQL injection in field names
  • Reject non-existent fields
  • Reject prototype pollution: __proto__, constructor

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

  • Wildcard injection: %' OR '1'='1
  • Underscore injection: test_' OR '1'='1
  • Backslash handling: test\\'; DROP--
  • Multiple wildcards: %_%'; DROP--
  • Case insensitive injection: '; UNION SELECT--

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

  • IN with malicious arrays: ['; DROP--', 'UNION SELECT--']
  • NOT IN with injection: ['; TRUNCATE--', 'DELETE FROM--']
  • Empty array handling
  • Large array validation (100+ items)
  • Mixed type array handling

Cas limites (edge-cases.test.ts)

  • Unicode injection: \u0027 OR \u00271\u0027=\u00271
  • Hex-encoded injection: 0x31=0x31--
  • URL encoded: %27%3B%20DROP%20TABLE
  • Stacked queries: '; DROP TABLE users; SELECT
  • Time-based blind (dialect-specific): WAITFOR DELAY '00:00:05'--
  • UNION-based: UNION ALL SELECT null, password
  • Second-order injection attempts

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

  • Sequential placeholder positions
  • Parameter array matches placeholder order
  • Complex queries maintain order across conditions
  • Nested OR/AND conditions preserve parameter sequence
  • Relation filters maintain correct parameter mapping

Sûr vs dangereux : comparaison de code

❌ Dangereux : concaténation de chaînes

const email = req.body.email
const sql = "SELECT * FROM users WHERE email = '" + email + "'"

✅ Sûr : liaison automatique des paramètres

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 utilisateur est liée et stable en ordre

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

Validation et quotation conscientes du dialecte pour prévenir l’injection via 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 critiques.

Do: Utiliser la bibliothèque avec la validation complète
Don't: 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 la base.

Do: Exécuter prisma db pull et prisma generate après des changements
Don't: Utiliser un DMMF ou schéma obsolète

Valider les types d’entrée

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

Do: Utiliser les types générés par Prisma
Don't: Caster l’entrée utilisateur en any avant de la passer aux requêtes

Surveiller les patterns de requêtes

Logger et surveiller SQL généré et paramètres en production pour détecter des anomalies.

Do: Activer le logging et revoir les patterns
Don't: Exécuter en production sans monitoring

Sécurité par conception

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