Sécurité et droits#
Le modèle de sécurité SQL#
La sécurité d’une base de données relationnelle repose sur trois concepts fondamentaux : les utilisateurs (ou rôles), les objets (tables, vues, schémas, séquences…) et les privilèges qui définissent quelles actions un utilisateur est autorisé à effectuer sur quels objets.
Définition 98
Le modèle de sécurité SQL est fondé sur le contrôle d’accès discrétionnaire (DAC — Discretionary Access Control) : le propriétaire d’un objet peut accorder et révoquer des droits sur cet objet à d’autres utilisateurs ou rôles. Les privilèges sont accordés via GRANT et révoqués via REVOKE.
Les principaux privilèges SQL sur les tables sont :
Privilège |
Signification |
|---|---|
|
Lire les données |
|
Insérer des lignes |
|
Modifier des lignes |
|
Supprimer des lignes |
|
Créer des clés étrangères vers la table |
|
Créer des triggers sur la table |
|
Tous les privilèges |
Remarque 56
Dans PostgreSQL, un utilisateur est un rôle avec la capacité de se connecter (LOGIN). Un rôle au sens large peut représenter un groupe d’utilisateurs. La gestion des utilisateurs SQLite n’existe pas : SQLite est un fichier local, la sécurité est gérée par le système de fichiers.
GRANT et REVOKE#
Accorder des privilèges#
Définition 99
GRANT accorde des privilèges à un utilisateur ou un rôle. La syntaxe générale est :
GRANT privilege [, ...]
ON objet
TO utilisateur [, ...] [WITH GRANT OPTION];
L’option WITH GRANT OPTION permet au bénéficiaire de transmettre à son tour le privilège à d’autres.
Exemples illustratifs (PostgreSQL) :
-- Autoriser alice à lire la table employes
GRANT SELECT ON employes TO alice;
-- Autoriser bob à lire et modifier
GRANT SELECT, UPDATE ON employes TO bob;
-- Accorder tous les droits sur un schéma
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO admin_role;
-- Autoriser alice à transmettre le droit de lecture
GRANT SELECT ON commandes TO alice WITH GRANT OPTION;
Restreindre à certaines colonnes#
Remarque 57
Il est possible d’accorder des privilèges colonne par colonne, ce qui affine le contrôle d’accès :
-- alice ne peut lire que nom et prenom, pas le salaire
GRANT SELECT (nom, prenom) ON employes TO alice;
-- bob peut mettre à jour uniquement le statut
GRANT UPDATE (statut) ON commandes TO bob;
Cette granularité est utile pour respecter le principe de moindre privilège (least privilege).
Révoquer des privilèges#
Définition 100
REVOKE retire des privilèges précédemment accordés. Si le privilège avait été accordé avec WITH GRANT OPTION, on peut aussi révoquer la capacité de transmission avec CASCADE :
REVOKE SELECT ON employes FROM alice;
REVOKE SELECT ON commandes FROM alice CASCADE;
Rôles : CREATE ROLE et héritage#
Les rôles permettent de gérer des groupes de permissions. Au lieu d’attribuer les mêmes droits à dix utilisateurs un par un, on crée un rôle, on lui donne les droits, et on attribue le rôle aux utilisateurs.
Définition 101
Un rôle PostgreSQL est un objet de la base qui peut recevoir des privilèges et être attribué à d’autres rôles ou utilisateurs. La hiérarchie des rôles permet un héritage des privilèges : si alice possède le rôle lecteur, elle hérite de tous les privilèges accordés à lecteur.
Les blocs suivants supposent une connexion à un serveur PostgreSQL.
-- Créer un rôle sans droit de connexion (groupe)
CREATE ROLE lecteur;
CREATE ROLE editeur;
CREATE ROLE admin_bdd;
-- Accorder des droits au rôle
GRANT SELECT ON ALL TABLES IN SCHEMA public TO lecteur;
GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO editeur;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO admin_bdd;
-- Créer des utilisateurs et leur attribuer des rôles
CREATE USER alice WITH LOGIN PASSWORD 'motdepasse_alice';
CREATE USER bob WITH LOGIN PASSWORD 'motdepasse_bob';
GRANT lecteur TO alice;
GRANT editeur TO bob;
-- Héritage : alice hérite des SELECT de lecteur
-- bob hérite des SELECT, INSERT, UPDATE de editeur
Remarque 58
Par défaut dans PostgreSQL, un rôle hérite des privilèges des rôles qui lui sont attribués. On peut désactiver cet héritage avec NOINHERIT lors de la création du rôle. Dans ce cas, l’utilisateur doit explicitement SET ROLE lecteur; pour activer les droits du rôle parent.
Row-Level Security (RLS)#
La sécurité au niveau ligne (Row-Level Security, RLS) permet de restreindre les lignes qu’un utilisateur peut voir ou modifier, selon son identité.
Définition 102
La sécurité au niveau ligne (RLS, Row-Level Security) est une fonctionnalité PostgreSQL qui permet d’attacher des politiques à une table. Chaque politique définit une condition (USING) qui filtre les lignes visibles, et optionnellement une condition WITH CHECK pour les écritures. Tant que RLS n’est pas activé sur une table, les politiques n’ont aucun effet.
Exemple illustratif (PostgreSQL — requiert un serveur PostgreSQL) :
-- Activer RLS sur la table commandes
ALTER TABLE commandes ENABLE ROW LEVEL SECURITY;
-- Chaque utilisateur ne voit que ses propres commandes
CREATE POLICY politique_commandes_utilisateur
ON commandes
FOR ALL
TO PUBLIC
USING (client = current_user);
-- Les administrateurs voient tout (politique distincte)
CREATE POLICY politique_admin
ON commandes
FOR ALL
TO admin_bdd
USING (TRUE);
Remarque 59
Le propriétaire de la table contourne toujours les politiques RLS par défaut. Pour soumettre aussi le propriétaire aux politiques, on utilise ALTER TABLE ... FORCE ROW LEVEL SECURITY. Les superutilisateurs contournent toujours RLS.
Injection SQL : mécanisme et prévention#
L’injection SQL est l’une des vulnérabilités les plus répandues et les plus dangereuses dans les applications web.
Définition 103
Une injection SQL survient lorsqu’une application construit dynamiquement une requête SQL en concaténant des données provenant de l’utilisateur sans les valider ni les échapper. Un attaquant peut alors injecter du code SQL arbitraire dans la requête, modifiant son comportement : extraction de données confidentielles, contournement de l’authentification, modification ou destruction de données.
Démonstration d’une injection#
# Mise en place d'une petite base d'exemple
conn_demo = sqlite3.connect(":memory:")
cur_demo = conn_demo.cursor()
cur_demo.executescript("""
CREATE TABLE utilisateurs (
id INTEGER PRIMARY KEY,
login TEXT UNIQUE NOT NULL,
mdp_hash TEXT NOT NULL,
role TEXT DEFAULT 'user'
);
INSERT INTO utilisateurs VALUES
(1, 'alice', 'hachage_alice', 'user'),
(2, 'bob', 'hachage_bob', 'user'),
(3, 'admin', 'hachage_admin', 'admin');
""")
conn_demo.commit()
# CODE DANGEREUX — NE JAMAIS FAIRE EN PRODUCTION
# Simulation d'une fonction de connexion vulnérable
def connexion_vulnerable(login_saisi, mdp_saisi):
"""Construit la requête par concaténation — DANGEREUX."""
requete = (
f"SELECT id, login, role FROM utilisateurs "
f"WHERE login = '{login_saisi}' AND mdp_hash = '{mdp_saisi}'"
)
print(f" Requête générée : {requete}")
rows = cur_demo.execute(requete).fetchall()
return rows
print("=== Connexion normale ===")
print(connexion_vulnerable("alice", "hachage_alice"))
print("\n=== Injection SQL — bypass d'authentification ===")
# L'attaquant saisit : ' OR '1'='1' --
# La requête devient : WHERE login = '' OR '1'='1' -- ' AND ...
# Le commentaire -- annule la vérification du mot de passe
resultat = connexion_vulnerable("' OR '1'='1' --", "peu_importe")
print(f" Lignes retournées : {resultat}")
=== Connexion normale ===
Requête générée : SELECT id, login, role FROM utilisateurs WHERE login = 'alice' AND mdp_hash = 'hachage_alice'
[(1, 'alice', 'user')]
=== Injection SQL — bypass d'authentification ===
Requête générée : SELECT id, login, role FROM utilisateurs WHERE login = '' OR '1'='1' --' AND mdp_hash = 'peu_importe'
Lignes retournées : [(1, 'alice', 'user'), (2, 'bob', 'user'), (3, 'admin', 'admin')]
Remarque 60
Dans l’exemple ci-dessus, l’attaquant accède à la liste complète des utilisateurs sans connaître aucun mot de passe. Des variantes plus destructrices permettent de :
Exfiltrer des données avec
UNION SELECTsur d’autres tables.Modifier des données avec des requêtes
UPDATEouDELETEempilées.Lister le schéma avec
sqlite_master(SQLite) ouinformation_schema(autres SGBD).
Requêtes paramétrées : la solution#
Définition 104
Une requête paramétrée (parameterized query ou prepared statement) sépare la structure SQL des données. Les valeurs sont passées comme paramètres distincts — jamais interpolées dans la chaîne SQL. Le moteur SQL traite les paramètres comme des données littérales, pas comme du code ; l’injection devient structurellement impossible.
# BONNE PRATIQUE — requête paramétrée avec sqlite3
def connexion_securisee(login_saisi, mdp_saisi):
"""Utilise des marqueurs ? — les données ne sont jamais du code SQL."""
requete = (
"SELECT id, login, role FROM utilisateurs "
"WHERE login = ? AND mdp_hash = ?"
)
rows = cur_demo.execute(requete, (login_saisi, mdp_saisi)).fetchall()
return rows
print("=== Connexion normale ===")
print(connexion_securisee("alice", "hachage_alice"))
print("\n=== Tentative d'injection avec requête paramétrée ===")
# La chaîne malveillante est traitée comme une valeur littérale
resultat = connexion_securisee("' OR '1'='1' --", "peu_importe")
print(f" Lignes retournées : {resultat}")
print(" (Résultat vide : l'injection a échoué)")
=== Connexion normale ===
[(1, 'alice', 'user')]
=== Tentative d'injection avec requête paramétrée ===
Lignes retournées : []
(Résultat vide : l'injection a échoué)
Remarque 61
Les ORM (SQLAlchemy, Django ORM, Hibernate) utilisent systématiquement des requêtes paramétrées. Ils éliminent ce risque pour les opérations CRUD classiques. En revanche, les requêtes construites manuellement avec du SQL brut dans un ORM peuvent réintroduire la vulnérabilité si elles concatènent des données utilisateur.
Trigger d’audit complet#
# Reprise de la démonstration avec un audit complet
conn_audit = sqlite3.connect(":memory:")
cur_audit = conn_audit.cursor()
cur_audit.executescript("""
CREATE TABLE comptes (
id INTEGER PRIMARY KEY,
login TEXT NOT NULL,
solde REAL DEFAULT 0
);
CREATE TABLE audit_acces (
id INTEGER PRIMARY KEY AUTOINCREMENT,
table_cible TEXT,
operation TEXT,
login_modif TEXT,
detail TEXT,
ts TEXT DEFAULT (datetime('now'))
);
INSERT INTO comptes VALUES (1, 'alice', 1500.00);
INSERT INTO comptes VALUES (2, 'bob', 800.00);
-- Trigger d'audit sur UPDATE
CREATE TRIGGER audit_update_comptes
AFTER UPDATE ON comptes
FOR EACH ROW
BEGIN
INSERT INTO audit_acces (table_cible, operation, login_modif, detail)
VALUES (
'comptes',
'UPDATE',
OLD.login,
'solde : ' || OLD.solde || ' -> ' || NEW.solde
);
END;
-- Trigger d'audit sur DELETE
CREATE TRIGGER audit_delete_comptes
AFTER DELETE ON comptes
FOR EACH ROW
BEGIN
INSERT INTO audit_acces (table_cible, operation, login_modif, detail)
VALUES (
'comptes',
'DELETE',
OLD.login,
'compte supprimé (solde=' || OLD.solde || ')'
);
END;
""")
conn_audit.commit()
# Opérations
cur_audit.execute("UPDATE comptes SET solde = 2000 WHERE login = 'alice'")
cur_audit.execute("UPDATE comptes SET solde = 600 WHERE login = 'bob'")
cur_audit.execute("DELETE FROM comptes WHERE login = 'bob'")
conn_audit.commit()
print("=== Journal d'audit ===")
display(pd.read_sql("SELECT * FROM audit_acces", conn_audit))
=== Journal d'audit ===
| id | table_cible | operation | login_modif | detail | ts | |
|---|---|---|---|---|---|---|
| 0 | 1 | comptes | UPDATE | alice | solde : 1500.0 -> 2000.0 | 2026-03-31 22:47:13 |
| 1 | 2 | comptes | UPDATE | bob | solde : 800.0 -> 600.0 | 2026-03-31 22:47:13 |
| 2 | 3 | comptes | DELETE | bob | compte supprimé (solde=600.0) | 2026-03-31 22:47:13 |
Chiffrement#
Remarque 62
Le chiffrement dans les bases de données s’applique à plusieurs niveaux :
Chiffrement au repos (at rest) : les fichiers de la base de données sont chiffrés sur le disque. PostgreSQL délègue cela au système de fichiers (LUKS, FileVault) ou à des extensions commerciales (Transparent Data Encryption dans PostgreSQL Enterprise). SQLite dispose de SQLCipher.
Chiffrement en transit (in transit) : les communications entre l’application et le serveur de base de données sont chiffrées via TLS/SSL. Dans PostgreSQL :
ssl = ondanspostgresql.conf; la connexion utilisesslmode=requirecôté client.Chiffrement de colonnes : certaines colonnes sensibles (numéros de carte bancaire, données de santé) sont chiffrées au niveau applicatif avant insertion. La base ne stocke et ne voit que le chiffré.
Exemple 37
Le chiffrement au niveau colonne en Python avec cryptography :
from cryptography.fernet import Fernet
import sqlite3
cle = Fernet.generate_key()
f = Fernet(cle)
conn = sqlite3.connect("ma_base.db")
conn.execute("CREATE TABLE patients (id INTEGER PRIMARY KEY, num_secu TEXT)")
num_secu = "1 85 12 75 056 147 80"
chiffre = f.encrypt(num_secu.encode()).decode()
conn.execute("INSERT INTO patients VALUES (1, ?)", (chiffre,))
# Lecture et déchiffrement
row = conn.execute("SELECT num_secu FROM patients WHERE id=1").fetchone()
original = f.decrypt(row[0].encode()).decode()
print(original) # "1 85 12 75 056 147 80"
La base ne contient que le texte chiffré ; sans la clé, les données sont inaccessibles.
Audit : logs d’accès#
Remarque 63
Dans PostgreSQL, le module pgaudit permet de journaliser précisément les requêtes SQL exécutées. On peut configurer l’audit par objet, par utilisateur ou par type d’opération. Les logs sont écrits dans les journaux PostgreSQL (pg_log) et peuvent être envoyés vers un SIEM.
Configuration minimale dans postgresql.conf :
shared_preload_libraries = 'pgaudit'
pgaudit.log = 'write, ddl' -- journalise écritures et DDL
pgaudit.log_relation = on -- précise la table concernée
SQLite ne dispose pas de gestion native des logs d’accès. L’audit est entièrement à implémenter via des triggers (comme démontré dans ce chapitre) ou au niveau applicatif.
Visualisation : hiérarchie des privilèges#
Résumé#
Remarque 64
Ce chapitre a couvert les mécanismes essentiels de sécurité SQL :
Contrôle d’accès :
GRANTaccorde des privilèges sur des objets ;REVOKEles retire.Les rôles permettent de gérer des groupes de permissions réutilisables.
Les privilèges peuvent être définis au niveau table, colonne ou schéma.
WITH GRANT OPTIONpermet la délégation de privilèges.
Sécurité avancée (PostgreSQL) :
La Row-Level Security (RLS) filtre les lignes accessibles selon la politique et l’identité de l’utilisateur.
Les politiques
USING(lecture) etWITH CHECK(écriture) définissent les conditions d’accès.
Injection SQL :
L’injection survient quand des données utilisateur sont concaténées dans une requête SQL.
La parade universelle est la requête paramétrée : les données ne sont jamais du code.
Les ORM protègent nativement contre ce risque pour les opérations standard.
Chiffrement et audit :
Chiffrement at rest (fichiers), in transit (TLS) et de colonnes (niveau applicatif).
Les triggers d’audit tracent les modifications sensibles dans une table de log.
PostgreSQL propose
pgauditpour un audit fin des requêtes.
conn_demo.close()
conn_audit.close()