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 ?)
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.
Garder le schéma à jour
Assurez-vous que votre schéma Prisma reflète fidèlement la structure de la base.
Valider les types d’entrée
TypeScript apporte la sécurité à la compilation, mais la validation runtime ajoute une défense en profondeur.
Surveiller les patterns de requêtes
Logger et surveiller SQL généré et paramètres en production pour détecter des anomalies.
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.