introduction
Dans SQL Server 2016, Microsoft a introduit le concept de sécurité au niveau des lignes, qui vous permet de contrôler avec précision qui a accès à quelles données, potentiellement jusqu’au niveau des lignes individuelles. Normalement, la sécurité SQL accorde un accès grossier à une table ou une vue entière (SQL Server peut également le faire pour les colonnes), et tout ce qui est plus granulaire que cela nécessite l’utilisation de vues ou de procédures stockées.
Il existe un autre moyen d’assurer la sécurité au niveau des lignes sans modifier les clients de la base de données, et c’est avec Gallium Data – un proxy de base de données gratuit qui peut modifier le trafic réseau entre vos clients de base de données et vos serveurs de base de données.
Chaque solution a ses points forts et ses faiblesses : comparons-les et découvrons laquelle correspond à quels scénarios.
L’essentiel pour les impatients :
- La sécurité au niveau des lignes de SQL Server présente le principal avantage d’être intégrée.
- Gallium Data a le principal avantage d’être beaucoup plus flexible.
Sécurité au niveau des lignes de SQL Server
Vous ajoutez une sécurité au niveau des lignes à SQL Server en enregistrant des fonctions SQL appelées prédicats, qui sont invoqués automatiquement lors de l’accès à une table donnée. Il existe deux sortes de prédicats :
- Filtrer les prédicats, qui sont utilisés pour les opérations de sélection, de mise à jour et de suppression.
- Bloquer les prédicats, qui sont utilisés pour l’insertion, la mise à jour et la suppression.
Lorsque SQL Server reçoit une commande SQL, il exécute ces prédicats avec l’opération et joint les résultats des prédicats aux tables pertinentes, vous permettant ainsi d’exclure certaines lignes.
Exemple de données
Nous utiliserons la base de données triviale suivante pour nos exemples :
create table demo.customers ( id int not null, name nvarchar(100) not null, country char(2) not null, primary key (id) ) insert into demo.customers values (1, 'Andrea', 'AR'), (2, 'Bruno', 'BE'), (3, 'Charles', 'CA'), (4, 'Daniella', 'DK'), (5, 'Eric', 'ES')
Filtrer les prédicats
Si nous supposons que les noms des utilisateurs de notre base de données commencent par un préfixe de région (par exemple I_jdoe), nous pouvons restreindre l’accès à cette table avec les éléments suivants :
create function demo.RestrictCustomers(@country as sysname) returns table with schemabinding as return select 1 as res where (@country in ('DK', 'BE', 'ES') and user_name() like 'EU_%') or (@country in ('US', 'CA') and user_name() like 'NA_%') or (suser_name() = 'sa')
Cela spécifie que les utilisateurs dont le nom commence par « EU_ » n’ont accès qu’aux clients au Danemark, en Belgique ou en Espagne, et les utilisateurs dont le nom commence par « NA_ » n’ont accès qu’aux clients aux États-Unis ou au Canada. Nous veillons également à ce que les utilisateurs à peut toujours voir tous les clients.
Ce type de fonction peut être aussi complexe que nécessaire et peut inclure des jointures et des appels à d’autres fonctions. Gardez cependant à l’esprit que tous l’accès à cette table entraînera le coût de cette fonction et de la jointure résultante.
Nous enregistrons notre fonction en tant que prédicat de filtre avec ce qui suit :
create security policy CustomersPolicy add filter predicate demo.RestrictCustomers(country) on demo.customers with (state = on)
Nous aurons besoin d’un utilisateur pour tester ceci comme ceci :
create user EU_jdoe without login grant select on demo.customers to EU_jdoe
Ensuite, si nous exécutons une requête simple comme ceci :
execute as user="EU_jdoe" select * from demo.customers revert
Nous ne récupérerons que les trois lignes de clients qui sont en Europe. Ça marche!
Notez que l’accès est accordé au niveau de la ligne : il n’y a aucun moyen de spécifier qu’une colonne spécifique dans une ligne spécifique est interdite à l’aide de ce mécanisme.
Bloquer les prédicats
Les prédicats de bloc sont similaires, mais ils s’appliquent aux insertions, mises à jour et/ou suppressions. Il en existe quatre sortes : après l’insertion, avant la mise à jour, après la mise à jour et avant la suppression.
Par exemple, nous pouvons nous assurer que nos utilisateurs ne peuvent insérer des données dans leur région qu’avec les éléments suivants :
alter security policy CustomersPolicy add block predicate demo.RestrictCustomers(country) on demo.customers after insert
Vous voudriez faire la même chose pour la mise à jour et la suppression. Cela vérifiera que la ligne nouvellement insérée est visible pour l’utilisateur qui l’a insérée et sinon, il rejettera l’insertion.
Avec ceci en place, un utilisateur peut insérer une ligne si elle obéit à notre prédicat :
execute as user="EU_jdoe" insert into demo.customers values (100, 'Didi', 'DK') revert
Mais, une insertion invalide est rejetée :
execute as user="EU_jdoe" insert into demo.customers values (101, 'Cameron', 'CA') revert
Avec le message d’erreur suivant :
The attempted operation failed because the target object 'demo.customers'
has a block predicate that conflicts with this operation etc...
Dans quelle mesure la sécurité au niveau des lignes de SQL Server est-elle sécurisée ?
Ce n’est pas à l’épreuve des balles. Par exemple, l’utilisateur I_jdoe n’a pas de visibilité pour le client 3 (qui est au Canada), mais peut détecter que la ligne existe avec un SQL intelligemment conçu :
execute as user="EU_jdoe" select * from demo.customers where 100/(id - 3) = 1 revert
Cela échouera (intentionnellement) avec le message d’erreur : Diviser par zéro erreur rencontrée, qui raconte I_jdoe qu’une ligne avec id=3 existe.
Vous pouvez aller beaucoup plus loin si vous pouvez exécuter un grand nombre de requêtes, par exemple :
execute as user="EU_jdoe" select * from demo.customers where 100/(patindex('Ch%', name) - 1) = 1 revert
Cela échouera avec le même message d’erreur, permettant à l’utilisateur I_jdoe pour déterminer qu’il y a au moins un client avec un Nom commençant par ‘Ch’, même si cet utilisateur n’est censé avoir aucune visibilité sur ce client. Il devrait être évident qu’un attaquant intelligent peut détecter des données assez rapidement.
Dans l’ensemble, je dirais que la sécurité au niveau des lignes n’est pas susceptible d’arrêter un acteur malveillant et devrait être davantage utilisée pour compléter la logique d’une application, par exemple pour externaliser les règles d’accès aux données.
Sécurité au niveau des lignes de Gallium Data
Gallium Data adopte une approche très différente. Il agit comme un proxy intelligent entre les clients de base de données et les serveurs de base de données et exécute une logique définie par l’utilisateur (exprimée sous forme de filtres et de code JavaScript) qui peut modifier ce trafic le cas échéant.
Pour la sécurité au niveau des lignes, Gallium Data dispose de deux options : modifier la commande SQL lors de son passage vers SQL Server ou filtrer l’ensemble de résultats provenant de SQL Server. Ces deux options peuvent être combinées.
Filtre de requête : modifier la commande SQL
Par exemple, un simple filtre de requête dans Gallium Data peut rechercher la déclaration :
select * from demo.customers where name like '?'
Et, réécrivez-le en :
select * from demo.customers where name like '?' and country in ('DK', 'BE', 'ES')
Selon l’utilisateur actuel. Cela fonctionne bien si vous savez à l’avance à quelles commandes SQL s’attendre.
Cette approche est efficace pour le filtrage à large bande et peut inclure une réécriture plus complexe pour masquer ou modifier les valeurs de colonne, par exemple :
select id, case when country in ('BE', 'DK', 'ES') and user_name() not like 'EU_%' then '<hidden>' when country in ('CA', 'US') and user_name() not like 'NA_%' then '<hidden>' else name end as name, country from demo.customers where name like '?'
Filtre de réponse : modifier le jeu de résultats
L’autre option (et souvent complémentaire) consiste à filtrer les jeux de résultats. Comme cela implique potentiellement l’exécution de code pour chaque ligne d’un jeu de résultats, il s’agit évidemment d’une solution plus coûteuse, mais très flexible. Le code JavaScript du filtre pourrait ressembler à ceci :
let country = context.packet.country; if (country === 'DK' || country === 'BE' || country === 'ES') { context.packet.remove(); }
C’est moins efficace que de changer la requête, mais c’est beaucoup plus flexible : on peut modifier la ligne, insérer de nouvelles lignes, etc. Par exemple, voir ci-dessous :
let country = context.packet.country; if (country === 'DK' || country === 'BE' || country === 'ES') { context.packet.name="<n/a>"; context.packet.country ='??'; }
Si vous devez filtrer un grand nombre de lignes (c. invoqué uniquement pour certaines lignes. Le filtre ci-dessus serait donc bien plus efficace s’il était défini avec le paramètre suivant :
Modèles de colonnes : pays=DK, pays=BE, pays=ES
Vous pouvez également spécifier que la ligne doit être masquée lorsque les modèles de colonne sont satisfaits. Cela permet un filtrage sans aucun code, similaire à la sécurité au niveau des lignes de SQL Server. Mais pour une logique plus complexe, JavaScript est la voie à suivre, car il vous donne une puissance totale.