data / sql
Sécurité : injection SQL et moindre privilège
Explication
Ce que vous allez apprendre
- Comprendre le mécanisme exact d'une injection SQL par concaténation de chaîne
- Se protéger systématiquement grâce aux requêtes paramétrées
- Sécuriser un identifiant dynamique (tri, colonne) avec une liste blanche
- Appliquer le principe du moindre privilège avec des rôles PostgreSQL dédiés
- Mettre en place une Row-Level Security pour restreindre l'accès aux lignes
Dans quel contexte ?
Un audit de sécurité découvre qu'un formulaire de connexion construit sa requête ainsi : "SELECT * FROM utilisateurs WHERE email = '" + email_saisi + "'". Un attaquant qui saisit ' OR '1'='1 dans le champ email obtient un accès à tous les comptes sans mot de passe. Cette leçon explique pourquoi cette faille existe et comment l'éliminer définitivement avec des requêtes paramétrées.
D'abord, comprendre la faille la plus simple à imaginer
Imagine une application qui construit ses requêtes SQL en collant directement le texte saisi par un utilisateur, par exemple dans un champ de connexion. Cette pratique paraît innocente au premier abord.
Le danger caché derrière cette simplicité
Un attaquant peut saisir un texte spécialement conçu qui change le SENS de la requête elle-même, au lieu d'être traité comme une simple valeur. C'est ce qu'on appelle l'injection SQL, l'une des vulnérabilités les plus anciennes du web, et pourtant toujours parmi les plus fréquentes.
Un exemple concret pour bien saisir l'ampleur du risque
Une phrase comme "OR '1'='1'" glissée dans un champ email peut transformer une vérification de mot de passe en un "montre-moi tous les utilisateurs, sans exception". Une simple boîte de texte devient une porte d'entrée vers toute la base.
La solution : séparer le code de la donnée
Les requêtes paramétrées, avec des marqueurs comme %s, ? ou :nom, résolvent le problème à la racine. La valeur saisie est envoyée séparément du texte SQL, et le pilote garantit qu'elle sera toujours traitée comme une simple donnée, jamais comme du code exécutable.
Piège fréquent
"SELECT * FROM utilisateurs WHERE email = '" + email + "'" avec email = "' OR '1'='1" devient SELECT * FROM utilisateurs WHERE email = '' OR '1'='1', une condition toujours vraie qui retourne TOUS les utilisateurs de la table, y compris leurs hachages de mot de passe.
Une fois les requêtes sécurisées, une question reste ouverte
Même avec des requêtes parfaitement protégées, que se passe-t-il si une faille survient ailleurs dans l'application ? Il faut limiter les dégâts possibles en amont.
La réponse : le principe du moindre privilège
Chaque connexion applicative ne reçoit que les droits dont elle a réellement besoin. Un service de lecture seule n'a besoin d'aucun droit d'écriture ; un service d'écriture limité n'a pas besoin de pouvoir supprimer des tables. Ainsi, même exploitée, une faille reste enfermée dans un périmètre restreint.
| Rôle | Droits accordés | Droits volontairement refusés |
|---|---|---|
app_lecture_seule | SELECT sur les tables nécessaires | INSERT, UPDATE, DELETE |
app_ecriture | SELECT, INSERT, UPDATE sur commandes | DELETE, DROP, accès à utilisateurs |
Superutilisateur (postgres/root) | Tous | Jamais utilisé par l'application elle-même |
Bonne pratique
Crée un rôle PostgreSQL distinct par service applicatif, avec uniquement les GRANT strictement nécessaires à sa fonction, et ne connecte jamais une application avec un compte superutilisateur.
Et pour finir, un principe qui dépasse le SQL
Le moindre privilège n'est pas une astuce propre au SQL : c'est une règle de sécurité générale qui s'applique à tous les systèmes informatiques. Ici, elle vient compléter les contraintes d'intégrité vues en leçon 8, qui protègent la cohérence des données, tandis que la sécurité protège leur confidentialité et leur accès.
Commandes & code
Sécurité : injection SQL et moindre privilège
-- DANGER : concaténation directe d'une entrée utilisateur dans le SQL
-- Si email_utilisateur = "' OR '1'='1", la requête devient toujours vraie
-- SELECT * FROM utilisateurs WHERE email = '' OR '1'='1'; -- fuite de TOUS les utilisateurs
-- NE JAMAIS FAIRE CELA :
-- query = "SELECT * FROM utilisateurs WHERE email = '" + email_utilisateur + "'"
-- BON : requête paramétrée (le driver échappe/traite la valeur séparément du SQL)
-- Exemple générique avec paramètres nommés/positionnels (psycopg2, PDO, JDBC...)
-- SELECT * FROM utilisateurs WHERE email = %s; -- psycopg2 (Python)
-- SELECT * FROM utilisateurs WHERE email = :email; -- SQLAlchemy / Oracle
-- SELECT * FROM utilisateurs WHERE email = ?; -- SQLite / JDBC
-- Injection via ORDER BY dynamique : valider contre une liste blanche, jamais concaténer
-- MAUVAIS : ORDER BY <colonne fournie par l'utilisateur> -> injection possible
-- BON : mapper l'entrée utilisateur vers une colonne autorisée en code applicatif
-- colonnes_autorisees = {"prix", "nom", "cree_le"}
-- si colonne_demandee not in colonnes_autorisees: refuser ou valeur par défaut
-- Principe du moindre privilège : chaque rôle applicatif n'a que les droits nécessaires
CREATE ROLE app_lecture_seule LOGIN PASSWORD 'change_moi';
GRANT CONNECT ON DATABASE technologik TO app_lecture_seule;
GRANT USAGE ON SCHEMA public TO app_lecture_seule;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_lecture_seule;
CREATE ROLE app_ecriture LOGIN PASSWORD 'change_moi_aussi';
GRANT SELECT, INSERT, UPDATE ON commandes, lignes_commande TO app_ecriture;
-- volontairement PAS de DELETE, PAS de DROP, PAS d'accès à la table utilisateurs
-- Révoquer des droits excessifs
REVOKE DELETE ON produits FROM app_ecriture;
REVOKE ALL ON utilisateurs FROM PUBLIC; -- retire tout accès par défaut
-- Row-level security : restreindre les lignes visibles selon l'utilisateur connecté (PostgreSQL)
ALTER TABLE commandes ENABLE ROW LEVEL SECURITY;
CREATE POLICY client_voit_ses_commandes ON commandes
FOR SELECT
USING (client_id = current_setting('app.current_client_id')::INTEGER);
-- Ne jamais stocker de mot de passe en clair : hacher avec un algorithme lent et salé
-- (le hachage se fait côté application, jamais en clair en base)
CREATE TABLE utilisateurs (
id INTEGER PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
mot_de_passe_hash VARCHAR(255) NOT NULL -- ex: résultat de bcrypt/argon2
);
-- Auditer les connexions et requêtes sensibles (log_statement, extension pgaudit)
ALTER SYSTEM SET log_statement = 'mod'; -- log tout INSERT/UPDATE/DELETE/DDL
-- Limiter la surface d'attaque : ne jamais exposer un compte superutilisateur à l'application
-- L'application se connecte avec app_ecriture, jamais avec le rôle "postgres"/"root"Résumé
- Les requêtes paramétrées éliminent l'injection SQL ; la concaténation de chaînes ne doit JAMAIS contenir d'entrée utilisateur.
- Valider les identifiants dynamiques (colonnes/tables) contre une liste blanche, jamais les injecter tels quels.
- Principe du moindre privilège : un rôle applicatif par usage, avec uniquement les
GRANTnécessaires. - Row-Level Security restreint l'accès aux lignes au niveau base, indépendamment du code applicatif.
- Les mots de passe sont toujours hachés (bcrypt/argon2) côté application, jamais stockés en clair.
Exercices pratiques
Mission : colmater une faille d'injection critique avant la prochaine attaque
Objectif : Corriger une injection SQL avérée et cloisonner les accès des services applicatifs avec le moindre privilège.
Contexte
Un audit de sécurité vient de découvrir qu'un formulaire de connexion construit sa requête par concaténation directe : "SELECT * FROM utilisateurs WHERE email = '" + email_saisi + "'". De plus, l'application se connecte actuellement à la base avec le rôle superutilisateur, ce qui aggrave l'impact potentiel de toute faille future.