sql >> Database >  >> RDS >> PostgreSQL

Applicatiegebruikers versus beveiliging op rijniveau

Een paar dagen geleden heb ik geblogd over de veelvoorkomende problemen met rollen en privileges die we ontdekken tijdens beveiligingsbeoordelingen.

Natuurlijk biedt PostgreSQL veel geavanceerde beveiligingsgerelateerde functies, waaronder Row Level Security (RLS), beschikbaar sinds PostgreSQL 9.5.

Aangezien 9.5 in januari 2016 werd uitgebracht (dus nog maar een paar maanden geleden), is RLS een vrij nieuwe functie en hebben we nog niet echt te maken met veel productie-implementaties. In plaats daarvan is RLS een veelvoorkomend onderwerp van discussies over "hoe te implementeren", en een van de meest voorkomende vragen is hoe het te laten werken met gebruikers op applicatieniveau. Dus laten we eens kijken welke mogelijke oplossingen er zijn.

Inleiding tot RLS

Laten we eerst een heel eenvoudig voorbeeld bekijken, waarin wordt uitgelegd waar RLS over gaat. Laten we zeggen dat we een chat hebben tabel waarin berichten worden opgeslagen die tussen gebruikers zijn verzonden - de gebruikers kunnen er rijen in invoegen om berichten naar andere gebruikers te verzenden, en ernaar vragen om berichten te zien die door andere gebruikers naar hen zijn verzonden. Dus de tabel kan er als volgt uitzien:

CREATE TABLE chat ( message_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(), message_time TIMESTAMP NOT NULL DEFAULT now(), message_from NAME NOT NULL DEFAULT current_user, message_to NAME NOT NULL, message_subject VARCHAR(64) NOT NULL, message_body TEXT); /pre> 

De klassieke op rollen gebaseerde beveiliging stelt ons alleen in staat om de toegang tot de hele tabel of verticale segmenten ervan (kolommen) te beperken. We kunnen het dus niet gebruiken om te voorkomen dat gebruikers berichten lezen die bedoeld zijn voor andere gebruikers, of berichten verzenden met een nep message_from veld.

En dat is precies waar RLS voor is:het stelt u in staat regels (beleidsregels) te maken die de toegang tot subsets van rijen beperken. U kunt dit bijvoorbeeld doen:

BELEID chat_policy OP chat MAKEN MET ((message_to =huidige_gebruiker) OF (message_from =huidige_gebruiker)) MET CONTROLE (message_from =huidige_gebruiker)

Dit beleid zorgt ervoor dat een gebruiker alleen berichten kan zien die door hem zijn verzonden of voor hem zijn bedoeld - dat is de voorwaarde in USING clausule doet. Het tweede deel van het beleid (WITH CHECK ) zorgt ervoor dat een gebruiker alleen berichten kan invoegen met zijn gebruikersnaam in message_from kolom, berichten met een vervalste afzender voorkomen.

Je kunt RLS ook zien als een automatische manier om extra WHERE-voorwaarden toe te voegen. Dat kon je handmatig doen op applicatieniveau (en voorheen deden mensen dat vaak bij RLS), maar RLS doet dat op een betrouwbare en veilige manier (er is veel energie gestoken in het voorkomen van bijvoorbeeld verschillende informatielekken).

Opmerking :Vóór RLS was een populaire manier om iets soortgelijks te bereiken, de tabel direct ontoegankelijk te maken (alle privileges intrekken) en een set beveiligingsdefinitiefuncties te bieden om er toegang toe te krijgen. Dat heeft grotendeels hetzelfde doel bereikt, maar functies hebben verschillende nadelen - ze hebben de neiging om de optimizer in de war te brengen en de flexibiliteit ernstig te beperken (als de gebruiker iets moet doen en er geen geschikte functie voor is, heeft hij pech). En natuurlijk moet je die functies schrijven.

Applicatiegebruikers

Als je de officiële documentatie over RLS leest, valt je misschien één detail op:alle voorbeelden gebruiken current_user , d.w.z. de huidige databasegebruiker. Maar zo werken de meeste databasetoepassingen tegenwoordig niet. Webapplicaties met veel geregistreerde gebruikers onderhouden geen 1:1-toewijzing aan databasegebruikers, maar gebruiken in plaats daarvan een enkele databasegebruiker om zelf query's uit te voeren en applicatiegebruikers te beheren - misschien in een users tafel.

Technisch gezien is het geen probleem om veel databasegebruikers aan te maken in PostgreSQL. De database zou dat zonder problemen moeten kunnen, maar applicaties doen dat om een ​​aantal praktische redenen niet. Ze moeten bijvoorbeeld aanvullende informatie voor elke gebruiker bijhouden (bijv. afdeling, functie binnen de organisatie, contactgegevens, ...), zodat de applicatie de users nodig heeft. tafel toch.

Een andere reden kan het poolen van verbindingen zijn - het gebruik van een enkel gedeeld gebruikersaccount, hoewel we weten dat dit kan worden opgelost met behulp van overerving en SET ROLE (zie het vorige bericht).

Maar laten we aannemen dat u geen afzonderlijke databasegebruikers wilt maken - u wilt één gedeeld database-account blijven gebruiken en RLS gebruiken met toepassingsgebruikers. Hoe doe je dat?

Sessievariabelen

Wat we in wezen nodig hebben, is om extra context door te geven aan de databasesessie, zodat we deze later kunnen gebruiken vanuit het beveiligingsbeleid (in plaats van de current_user variabel). En de gemakkelijkste manier om dat in PostgreSQL te doen, zijn sessievariabelen:

SET my.username ='tomas'

Als dit lijkt op de gebruikelijke configuratieparameters (bijv. SET work_mem = '...' ), je hebt helemaal gelijk - het is grotendeels hetzelfde. De opdracht definieert een nieuwe naamruimte (my ), en voegt een username . toe variabel erin. De nieuwe naamruimte is vereist, omdat de globale is gereserveerd voor de serverconfiguratie en we er geen nieuwe variabelen aan kunnen toevoegen. Dit stelt ons in staat om het beveiligingsbeleid als volgt te wijzigen:

MAAK BELEID chat_policy OP chat MET GEBRUIK VAN (current_setting('my.username') IN (message_from, message_to)) MET CONTROLE (message_from =current_setting('my.username'))

Het enige wat we hoeven te doen is ervoor te zorgen dat de verbindingspool / toepassing de gebruikersnaam instelt wanneer deze een nieuwe verbinding krijgt en deze toewijst aan de gebruikerstaak.

Ik wil erop wijzen dat deze benadering instort zodra u de gebruikers toestaat willekeurige SQL op de verbinding uit te voeren, of als de gebruiker erin slaagt een geschikte kwetsbaarheid voor SQL-injectie te ontdekken. In dat geval is er niets dat hen ervan kan weerhouden een willekeurige gebruikersnaam in te stellen. Maar wanhoop niet, er zijn een heleboel oplossingen voor dat probleem, en we zullen ze snel doornemen.

Ondertekende sessievariabelen

De eerste oplossing is een eenvoudige verbetering van de sessievariabelen - we kunnen niet echt voorkomen dat de gebruikers een willekeurige waarde instellen, maar wat als we konden verifiëren dat de waarde niet werd ondermijnd? Dat is vrij eenvoudig te doen met een eenvoudige digitale handtekening. In plaats van alleen de gebruikersnaam op te slaan, kan het vertrouwde deel (verbindingspool, applicatie) zoiets als dit doen:

handtekening =sha256(gebruikersnaam + tijdstempel + GEHEIM)

en sla vervolgens zowel de waarde als de handtekening op in de sessievariabele:

SET my.username ='username:timestamp:signature'

Ervan uitgaande dat de gebruiker de SECRET-reeks niet kent (bijv. 128B aan willekeurige gegevens), zou het niet mogelijk moeten zijn om de waarde te wijzigen zonder de handtekening ongeldig te maken.

Opmerking :Dit is geen nieuw idee - het is in wezen hetzelfde als ondertekende HTTP-cookies. Django heeft daar een aardige documentatie over.

De eenvoudigste manier om de SECRET-waarde te beschermen, is door deze op te slaan in een tabel die niet toegankelijk is voor de gebruiker, en een security definer aan te bieden. functie, waarvoor een wachtwoord vereist is (zodat de gebruiker niet zomaar willekeurige waarden kan ondertekenen).

CREATE FUNCTIE set_username(uname TEXT, pwd TEXT) TERUGZET tekst ALS $DECLARE v_key TEXT; v_value TEXT;BEGIN SELECT sign_key INTO v_key FROM geheimen; v_value :=uname || ':' || extract(epoch from now())::int; v_waarde :=v_waarde || ':' || crypt(v_value || ':' || v_key, gen_salt('bf')); PERFORM set_config('my.username', v_value, false); RETURN v_value;END;$ TAAL plpgsql SECURITY DEFINER STABLE;

De functie zoekt eenvoudig de ondertekeningssleutel (geheim) op in een tabel, berekent de handtekening en stelt vervolgens de waarde in de sessievariabele in. Het geeft ook de waarde terug, meestal voor het gemak.

Dus het vertrouwde deel kan dit doen vlak voordat de verbinding aan de gebruiker wordt gegeven (uiteraard is 'passphrase' geen erg goed wachtwoord voor productie):

SELECT set_username('tomas', 'passphrase')

En dan hebben we natuurlijk nog een functie nodig die eenvoudig de handtekening verifieert en fouten maakt of de gebruikersnaam retourneert als de handtekening overeenkomt.

CREATE FUNCTIE get_username() RETURN tekst AS $DECLARE v_key TEXT; v_parts TEXT[]; v_uname TEKST; v_waarde TEKST; v_tijdstempel INT; v_signature TEXT;BEGIN -- geen wachtwoordverificatie deze keer SELECT sign_key INTO v_key FROM secrets; v_parts :=regexp_split_to_array(current_setting('my.username', true), ':'); v_uname :=v_parts[1]; v_timestamp :=v_parts[2]; v_signature :=v_parts[3]; v_waarde :=v_uname || ':' || v_timestamp || ':' || v_toets; IF v_signature =crypt (v_value, v_signature) DAN TERUG v_uname; STOP ALS; VERHOOG UITZONDERING 'ongeldige gebruikersnaam / tijdstempel';END;$ TAAL plpgsql SECURITY DEFINER STABLE;

En aangezien deze functie geen wachtwoordzin nodig heeft, kan de gebruiker dit eenvoudig doen:

SELECT get_username()

Maar de get_username() functie is bedoeld voor beveiligingsbeleid, b.v. zoals dit:

BELEID chat_policy OP chat MAKEN MET (get_username() IN (message_from, message_to)) MET CONTROLE (message_from =get_username())

Een completer voorbeeld, verpakt als een eenvoudige extensie, vindt u hier.

Merk op dat alle objecten (tabel en functies) eigendom zijn van een bevoorrechte gebruiker, niet van de gebruiker die toegang heeft tot de database. De gebruiker heeft alleen EXECUTE privilege op de functies, die echter zijn gedefinieerd als SECURITY DEFINER . Dat is wat ervoor zorgt dat dit schema werkt terwijl het het geheim voor de gebruiker beschermt. De functies zijn gedefinieerd als STABLE , om het aantal oproepen naar de crypt() te beperken functie (die opzettelijk duur is om bruteforcing te voorkomen).

De voorbeeldfuncties hebben zeker meer werk nodig. Maar hopelijk is het goed genoeg voor een proof-of-concept dat laat zien hoe extra context kan worden opgeslagen in een beveiligde sessievariabele.

Wat moet er gerepareerd worden vraag je? Ten eerste gaan de functies niet goed om met verschillende foutcondities. Ten tweede, hoewel de ondertekende waarde een tijdstempel bevat, doen we er niet echt iets mee - het kan bijvoorbeeld worden gebruikt om de waarde te laten verlopen. Het is mogelijk om extra bits aan de waarde toe te voegen, b.v. een afdeling van de gebruiker, of zelfs informatie over de sessie (bijv. PID van het backend-proces om te voorkomen dat dezelfde waarde opnieuw wordt gebruikt op andere verbindingen).

Crypto

De twee functies zijn afhankelijk van cryptografie - we gebruiken niet veel behalve enkele eenvoudige hash-functies, maar het is nog steeds een eenvoudig crypto-schema. En iedereen weet dat je niet je eigen crypto moet doen. Daarom heb ik de pgcrypto-extensie gebruikt, met name de crypt() functie, om dit probleem te omzeilen. Maar ik ben geen cryptograaf, dus hoewel ik denk dat het hele schema in orde is, mis ik misschien iets - laat het me weten als je iets ziet.

De ondertekening zou ook een goede match zijn voor cryptografie met openbare sleutels - we zouden een gewone PGP-sleutel met een wachtwoordzin kunnen gebruiken voor de ondertekening en het openbare deel voor handtekeningverificatie. Helaas, hoewel pgcrypto PGP ondersteunt voor codering, ondersteunt het de ondertekening niet.

Alternatieve benaderingen

Natuurlijk zijn er verschillende alternatieve oplossingen. In plaats van het ondertekeningsgeheim op te slaan in een tabel, kunt u het bijvoorbeeld hard coderen in de functie (maar dan moet u ervoor zorgen dat de gebruiker de broncode niet kan zien). Of u kunt het ondertekenen in een C-functie doen, in welk geval het verborgen is voor iedereen die geen toegang heeft tot het geheugen (in dat geval bent u toch verloren).

Als u de ondertekeningsaanpak helemaal niet leuk vindt, kunt u de ondertekende variabele vervangen door een meer traditionele "kluis" -oplossing. We hebben een manier nodig om de gegevens op te slaan, maar we moeten ervoor zorgen dat de gebruiker de inhoud niet willekeurig kan zien of wijzigen, behalve op een gedefinieerde manier. Maar goed, dat is wat reguliere tabellen met een API geïmplementeerd met behulp van security definer functies kunnen doen!

Ik ga hier niet het hele herwerkte voorbeeld presenteren (bekijk deze extensie voor een compleet voorbeeld), maar wat we nodig hebben is een sessions tafel die als kluis fungeert:

CREATE TABLE-sessies ( session_id UUID PRIMARY KEY, session_user NAME NOT NULL)

De tabel mag niet toegankelijk zijn voor gewone databasegebruikers - een simpele REVOKE ALL FROM ... moet daar voor zorgen. En dan een API die bestaat uit twee hoofdfuncties:

  • set_username(user_name, passphrase) – genereert een willekeurige UUID, voegt gegevens in de kluis in en slaat de UUID op in een sessievariabele
  • get_username() – leest de UUID van een sessievariabele en zoekt de rij op in de tabel (fouten als er geen overeenkomende rij is)

Deze benadering vervangt de handtekeningbeveiliging door willekeurigheid van de UUID - de gebruiker kan de sessievariabele aanpassen, maar de kans dat een bestaande ID wordt geraakt is verwaarloosbaar (UUID's zijn 128-bits willekeurige waarden).

Het is een wat traditionelere benadering, gebaseerd op traditionele, op rollen gebaseerde beveiliging, maar het heeft ook enkele nadelen:het schrijft bijvoorbeeld daadwerkelijk databases, wat betekent dat het inherent incompatibel is met hot-standby-systemen.

De wachtwoordzin verwijderen

Het is ook mogelijk om de kluis zo te ontwerpen dat de wachtwoordzin niet nodig is. We hebben het geïntroduceerd omdat we uitgingen van set_username gebeurt op dezelfde verbinding - we moeten de functie uitvoerbaar houden (dus knoeien met rollen of privileges is geen oplossing), en de wachtwoordzin zorgt ervoor dat alleen het vertrouwde onderdeel het daadwerkelijk kan gebruiken.

Maar wat als de ondertekening/sessie-aanmaak gebeurt op een aparte verbinding, en alleen het resultaat (ondertekende waarde of sessie-UUID) wordt gekopieerd naar de verbinding die aan de gebruiker wordt overhandigd? Nou, dan hebben we de wachtwoordzin niet meer nodig. (Het lijkt een beetje op wat Kerberos doet:een ticket genereren op een vertrouwde verbinding en het ticket vervolgens gebruiken voor andere services.)

Samenvatting

Dus laat me snel deze blogpost samenvatten:

  • Terwijl alle RLS-voorbeelden databasegebruikers gebruiken (door middel van current_user ), is het niet erg moeilijk om RLS te laten werken met applicatiegebruikers.
  • Sessievariabelen zijn een betrouwbare en vrij eenvoudige oplossing, ervan uitgaande dat het systeem een ​​vertrouwde component heeft die de variabele kan instellen voordat de verbinding aan een gebruiker wordt overgedragen.
  • Wanneer de gebruiker willekeurige SQL kan uitvoeren (hetzij door ontwerp of dankzij een kwetsbaarheid), voorkomt een ondertekende variabele dat de gebruiker de waarde wijzigt.
  • Andere oplossingen zijn mogelijk, b.v. de sessievariabelen vervangen door een tabel met informatie over sessies die worden geïdentificeerd door willekeurige UUID.
  • Het leuke is dat de sessievariabelen geen database-writes doen, dus deze aanpak kan werken op alleen-lezen systemen (bijv. hot standby).

In het volgende deel van deze blogserie zullen we kijken naar het gebruik van applicatiegebruikers wanneer het systeem geen vertrouwde component heeft (dus het kan de sessievariabele niet instellen of een rij maken in de sessions tabel), of wanneer we (aanvullende) aangepaste authenticatie binnen de database willen uitvoeren.


  1. Limieten voor Salesforce API Query-cursor

  2. PDO mysql-transacties begrijpen

  3. Hoe krijg ik de id na INSERT in MySQL-database met Python?

  4. Hoe gebruik je GROUP BY om strings samen te voegen in MySQL?