sql >> Database >  >> RDS >> PostgreSQL

Zelfvoorziening van gebruikersaccounts in PostgreSQL via onbevoegde anonieme toegang

Opmerking van Multiplenines:deze blog wordt postuum gepubliceerd toen Berend Tober op 16 juli 2018 overleed. We eren zijn bijdragen aan de PostgreSQL-gemeenschap en wensen onze vriend en gastschrijver vrede.

In vorig artikel hebben we de basisprincipes van PostgreSQL-triggers en opgeslagen functies geïntroduceerd en zes voorbeelden van gebruiksscenario's gegeven, waaronder gegevensvalidatie, logboekregistratie van wijzigingen, waarden afleiden uit ingevoegde gegevens, gegevens verbergen met eenvoudige bijwerkbare weergaven, overzichtsgegevens bijhouden in aparte tabellen, en veilig aanroepen van code bij verhoogde bevoegdheden. Dit artikel bouwt voort op die basis en presenteert een techniek die gebruik maakt van een trigger en een opgeslagen functie om het delegeren van inloggegevens naar rollen met beperkte bevoegdheden (d.w.z. niet-supergebruiker) te vergemakkelijken. Deze functie kan worden gebruikt om de administratieve werklast voor hoogwaardig systeembeheerpersoneel te verminderen. Tot het uiterste doorgevoerd, demonstreren we anonieme eindgebruikers die zelf inloggegevens verstrekken, d.w.z. potentiële databasegebruikers zelf inloggegevens laten verstrekken door "dynamische SQL" te implementeren in een opgeslagen functie die wordt uitgevoerd op het juiste privilegeniveau. Inleiding

Nuttige achtergrondinformatie

Het recente artikel van Sebastian Insausti over hoe u uw PostgreSQL-database kunt beveiligen, bevat enkele zeer relevante tips waarmee u bekend moet zijn, namelijk Tips #1 - #5 over Client Authentication Control, Server Configuration, User and Role Management, Super User Management en Data encryptie. We zullen delen van elke tip in dit artikel gebruiken.

Een ander recent artikel van Joshua Otwell over PostgreSQL Privileges &User Management heeft ook een goede behandeling van hostconfiguratie en gebruikersprivileges die wat meer in detail gaan op deze twee onderwerpen.

Netwerkverkeer beschermen

De voorgestelde functie houdt in dat gebruikers inloggegevens voor de database kunnen verstrekken en dat ze daarbij hun nieuwe inlognaam en wachtwoord via het netwerk zullen specificeren. Bescherming van deze netwerkcommunicatie is essentieel en kan worden bereikt door de PostgreSQL-server zo te configureren dat deze versleutelde verbindingen ondersteunt en vereist. Beveiliging van de transportlaag is ingeschakeld in het bestand postgresql.conf door de instelling "ssl":

ssl = on

Host-gebaseerde toegangscontrole

Voor het huidige geval zullen we een hostgebaseerde toegangsconfiguratieregel toevoegen aan het pg_hba.conf-bestand waarmee anoniem, d.w.z. vertrouwd, inloggen op de database mogelijk is vanaf een geschikt subnetwerk voor de populatie van potentiële databasegebruikers die letterlijk de gebruikersnaam gebruiken "anoniem", en een tweede configuratieregel die wachtwoordaanmelding vereist voor een andere inlognaam. Onthoud dat hostconfiguraties de eerste overeenkomst aanroepen, dus de eerste regel is van toepassing wanneer de "anonieme" gebruikersnaam is opgegeven, waardoor een vertrouwde (d.w.z. geen wachtwoord vereist) verbinding mogelijk is, en vervolgens wanneer een andere gebruikersnaam is opgegeven, is een wachtwoord vereist. Als de voorbeelddatabase "sampledb" bijvoorbeeld alleen door werknemers en intern naar bedrijfsfaciliteiten moet worden gebruikt, kunnen we vertrouwde toegang configureren voor een niet-routeerbaar intern subnet met:

# TYPE  DATABASE USER      ADDRESS        METHOD
hostssl sampledb anonymous 192.168.1.0/24 trust
hostssl sampledb all       192.168.1.0/24 md5

Als de database algemeen beschikbaar moet worden gemaakt voor het publiek, kunnen we toegang tot "elk adres" configureren:

# TYPE  DATABASE USER       ADDRESS  METHOD
hostssl sampledb anonymous  all      trust
hostssl sampledb all        all      md5

Houd er rekening mee dat het bovenstaande potentieel gevaarlijk is zonder extra voorzorgsmaatregelen, mogelijk in het ontwerp van de applicatie of bij een firewall-apparaat, om het gebruik van deze functie te beperken, omdat je weet dat een of andere scriptkiddie het maken van eindeloze accounts automatiseert, alleen voor de lulz.

Merk ook op dat we het verbindingstype hebben gespecificeerd als "hostssl", wat betekent dat verbindingen gemaakt met behulp van TCP/IP alleen slagen als de verbinding is gemaakt met SSL-codering om het netwerkverkeer te beschermen tegen afluisteren.

Het openbare schema afsluiten

Aangezien we mogelijk onbekende (d.w.z. niet-vertrouwde) personen toegang geven tot de database, willen we er zeker van zijn dat standaardtoegang beperkt is. Een belangrijke maatregel is het intrekken van het standaardrecht voor het maken van openbare schemaobjecten om een ​​recent gepubliceerde PostgreSQL-kwetsbaarheid met betrekking tot standaardschemarechten te verminderen (zie Het openbare schema vergrendelen door ondergetekende).

Een voorbeelddatabase

We beginnen met een lege voorbeelddatabase ter illustratie:

create database sampledb;
\connect sampledb

revoke create on schema public from public;
alter default privileges revoke all privileges on tables from public;

We creëren ook de anonieme inlogrol die overeenkomt met de eerdere pg_hba.conf-instelling.

create role anonymous login
    nosuperuser 
    noinherit 
    nocreatedb 
    nocreaterole 
    Noreplication;

En dan doen we iets nieuws door een onconventionele kijk te definiëren:

create or replace view person as 
 select 
    null::name as login_name,
    null::name as login_pass;

Deze weergave verwijst niet naar een tabel en daarom retourneert een selectiequery altijd een lege rij:

select * from person;
 login_name | login_pass 
------------+-------------
            | 
(1 row)

Een ding dat dit voor ons doet, is in zekere zin documentatie of een hint aan eindgebruikers geven over welke gegevens nodig zijn om een ​​account aan te maken. Dat wil zeggen, door de tabel te doorzoeken, hoewel het resultaat een lege rij is, onthult het resultaat de namen van de twee gegevenselementen.

Maar nog beter, het bestaan ​​van deze weergave maakt het mogelijk om de vereiste datatypes te bepalen:

\d person
      View "public.person"
    Column    | Type | Modifiers 
--------------+------+-----------
 login_name   | name | 
 login_pass   | name | 

We zullen de functionaliteit voor het verstrekken van referenties implementeren met een opgeslagen functie en trigger, dus laten we een lege functiesjabloon en de bijbehorende trigger declareren:

create or replace function person_iit()
  returns trigger
  set schema 'public'
  language plpgsql
  security definer
  as '
  begin
  end;
  ';

create trigger person_iit
  instead of insert
  on person
  for each row execute procedure person_iit();

Houd er rekening mee dat we de voorgestelde naamgevingsconventie uit het vorige artikel volgen, waarbij we de bijbehorende tabelnaam gebruiken met een achtervoegsel met een korte afkorting die kenmerken aangeeft van de triggerrelatie tussen de tabel en de opgeslagen functie voor een INSTEAD OF INSERT-trigger (d.w.z. achtervoegsel " iit"). We hebben ook aan de opgeslagen functie de kenmerken SCHEMA en SECURITY DEFINER toegevoegd:de eerste omdat het een goede gewoonte is om het zoekpad in te stellen dat van toepassing is op de duur van de functie-uitvoering, en de laatste om het maken van rollen te vergemakkelijken, wat normaal gesproken een database-superuser-autoriteit is alleen, maar in dit geval wordt het gedelegeerd aan anonieme gebruikers.

En als laatste voegen we minimaal voldoende machtigingen toe aan de weergave om te zoeken en in te voegen:

grant select, insert on table person to anonymous;
Download de whitepaper vandaag PostgreSQL-beheer en -automatisering met ClusterControlLees wat u moet weten om PostgreSQL te implementeren, bewaken, beheren en schalenDownload de whitepaper

Laten we eens kijken

Laten we, voordat we de opgeslagen functiecode implementeren, eens kijken wat we hebben. Ten eerste is er de voorbeelddatabase die eigendom is van de postgres-gebruiker:

\l
                                  List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges   
-----------+----------+----------+-------------+-------------+-----------------------
 sampledb  | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | 
And there’s the user roles, including the database superuser and the newly-created anonymous login roles:
\du
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 anonymous | No inheritance                                             | {}
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

En er is de weergave die we hebben gemaakt en een lijst met toegangsrechten voor maken en lezen die door de postgres-gebruiker aan de anonieme gebruiker zijn verleend:

\d
         List of relations
 Schema |  Name  | Type |  Owner   
--------+--------+------+----------
 public | person | view | postgres
(1 row)


\dp
                                Access privileges
 Schema |  Name  | Type |     Access privileges     | Column privileges | Policies 
--------+--------+------+---------------------------+-------------------+----------
 public | person | view | postgres=arwdDxt/postgres+|                   | 
        |        |      | anonymous=ar/postgres     |                   | 
(1 row)

Ten slotte toont het tabeldetail de kolomnamen en datatypes, evenals de bijbehorende trigger:

\d person
      View "public.person"
    Column    | Type | Modifiers 
--------------+------+-----------
 login_name   | name | 
 login_pass   | name | 
Triggers:
    person_iit INSTEAD OF INSERT ON person FOR EACH ROW EXECUTE PROCEDURE person_iit()

Dynamische SQL

We gaan dynamische SQL gebruiken, d.w.z. het construeren van de uiteindelijke vorm van een DDL-statement tijdens runtime, gedeeltelijk uit door de gebruiker ingevoerde gegevens, om de hoofdtekst van de triggerfunctie in te vullen. In het bijzonder coderen we de omtrek van het statement om een ​​nieuwe login-rol te creëren en vullen we de specifieke parameters in als variabelen.

De algemene vorm van dit commando is

create role name [ [ with ] option [ ... ] ]

waar optie kan elk van de zestien specifieke eigenschappen zijn. Over het algemeen zijn de standaardinstellingen geschikt, maar we zullen expliciet zijn over verschillende beperkende opties en het formulier gebruiken

create role name 
  with 
    login 
    inherit 
    nosuperuser 
    nocreatedb 
    nocreaterole 
    password ‘password’;

waar we tijdens runtime de door de gebruiker gespecificeerde rolnaam en wachtwoord invoegen.

Dynamisch geconstrueerde instructies worden aangeroepen met de opdracht execute:

execute command-string [ INTO [STRICT] target ] [ USING expression [, ... ] ];

die voor onze specifieke behoeften eruit zou zien

  execute 'create role '
    || new.login_name
    || ' with login inherit nosuperuser nocreatedb nocreaterole password '
    || quote_literal(new.login_pass);

waarbij de functie quote_literal het string-argument retourneert dat op de juiste manier wordt geciteerd voor gebruik als een letterlijke string om te voldoen aan de syntactische vereiste dat het wachtwoord in feite wordt geciteerd..

Zodra we de opdrachtreeks hebben gebouwd, leveren we deze als argument voor de opdracht pl/pgsql execute binnen de triggerfunctie.

Als je dit allemaal samenvoegt, ziet het er als volgt uit:

create or replace function person_iit()
  returns trigger
  set schema 'public'
  language plpgsql
  security definer
  as $$
  begin

  -- note this is for demonstration only. it is vulnerable to sql injection.

  execute 'create role '
    || new.login_name
    || ' with login inherit nosuperuser nocreatedb nocreaterole password '
    || quote_literal(new.login_pass);

  return new;
  end;
  $$;

Laten we het proberen!

Alles is op zijn plaats, dus laten we het wervelen! Eerst schakelen we sessie-autorisatie naar de anonieme gebruiker en doen dan een invoeging tegen de persoonsweergave:

set session authorization anonymous;
insert into person values ('alice', '1234');

Het resultaat is dat nieuwe gebruiker Alice is toegevoegd aan de systeemtabel:

\du
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 alice     |                                                            | {}
 anonymous | No inheritance                                             | {}
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

Het werkt zelfs rechtstreeks vanaf de opdrachtregel van het besturingssysteem door een SQL-opdrachtstring naar het psql-clienthulpprogramma te sturen om gebruiker bob toe te voegen:

$ psql sampledb anonymous <<< "insert into person values ('bob', '4321');"
INSERT 0 1

$ psql sampledb anonymous <<< "\du"
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 alice     |                                                            | {}
 anonymous | No inheritance                                             | {}
 bob       |                                                            | {}
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

Breng wat bepantsering aan

Het eerste voorbeeld van de triggerfunctie is kwetsbaar voor SQL-injectie-aanvallen, d.w.z. een kwaadwillende dreigingsactor zou input kunnen creëren die resulteert in ongeautoriseerde toegang. Bijvoorbeeld, terwijl verbonden als de anonieme gebruikersrol, mislukt een poging om iets buiten het bereik te doen op de juiste manier:

set session authorization anonymous;
drop user alice;
ERROR:  permission denied to drop role

Maar de volgende kwaadaardige invoer creëert een superuser-rol met de naam 'eve' (evenals een lokaccount met de naam 'cathy'):

insert into person 
  values ('eve with superuser login password ''666''; create role cathy', '777');
\du
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 alice     |                                                            | {}
 anonymous | No inheritance                                             | {}
 cathy     |                                                            | {}
 eve       | Superuser                                                  | {}
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

Dan kan de heimelijke superuser-rol worden gebruikt om schade aan te richten in de database, bijvoorbeeld door gebruikersaccounts te verwijderen (of erger!):

\c - eve
drop user alice;
\du
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 anonymous | No inheritance                                             | {}
 cathy     |                                                            | {}
 eve       | Superuser                                                  | {}
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

Om deze kwetsbaarheid te verkleinen, moeten we stappen ondernemen om de invoer te zuiveren. Bijvoorbeeld door de functie quote_ident toe te passen, die een tekenreeks retourneert die geschikt is geciteerd voor gebruik als identificatie in een SQL-instructie met indien nodig toegevoegde aanhalingstekens, bijvoorbeeld als de tekenreeks niet-identificatietekens bevat of hoofdletters zou zijn gevouwen, en op de juiste manier verdubbelt ingesloten citaten:

create or replace function person_iit()
  returns trigger
  set schema 'public'
  language plpgsql
  security definer
  as $$
  begin

  execute 'create role '
    || quote_ident(new.login_name)
    || ' with login inherit nosuperuser nocreatedb nocreaterole password '
    || quote_literal(new.login_pass);

  return new;
  end;
  $$;

Als nu wordt geprobeerd met dezelfde SQL-injectie-exploit een andere supergebruiker met de naam 'frank' te maken, mislukt dit en het resultaat is een zeer onorthodoxe gebruikersnaam:

set session authorization anonymous;
insert into person 
  values ('frank with superuser login password ''666''; create role dave', '777');
\du
                                 List of roles
    Role name          |                         Attributes                         | Member of 
-----------------------+------------------------------------------------------------+----------
 anonymous             | No inheritance                                             | {}
 eve                   | Superuser                                                  | {}
 frank with superuser  |                                                            |
  login password '666';|                                                            |
  create role dave     |                                                            |
 postgres              | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

We kunnen verdere verstandige gegevensvalidatie toepassen binnen de triggerfunctie, zoals het vereisen van alleen alfanumerieke gebruikersnamen en het weigeren van witruimte en andere tekens:

create or replace function person_iit()
  returns trigger
  set schema 'public'
  language plpgsql
  security definer
  as $$
  begin

  -- Basic input sanitization

  if new.login_name is null then
    raise exception 'null login_name disallowed';
  elsif position(' ' in new.login_name) > 0 then
    raise exception 'login_name whitespace disallowed';
  elsif length(new.login_name) = 0 then
    raise exception 'login_name must be non-empty';
  elsif not (select new.login_name similar to '[A-Za-z]%') then
    raise exception 'login_name must begin with a letter.';
  end if;

  if new.login_pass is null then
    raise exception 'null login_pass disallowed';
  elsif position(' ' in new.login_pass) > 0 then
    raise exception 'login_pass whitespace disallowed';
  elsif length(new.login_pass) = 0 then
    raise exception 'login_pass must be non-empty';
  end if;

  -- Provision login credentials

  execute 'create role '
    || quote_ident(new.login_name)
    || ' with login inherit nosuperuser nocreatedb nocreaterole password '
    || quote_literal(new.login_pass);

  return new;
  end;
  $$;

en bevestig vervolgens dat de verschillende desinfectiecontroles werken:

set session authorization anonymous;
insert into person values (NULL, NULL);
ERROR:  null login_name disallowed
insert into person values ('gina', NULL);
ERROR:  null login_pass disallowed
insert into person values ('gina', '');
ERROR:  login_pass must be non-empty
insert into person values ('', '1234');
ERROR:  login_name must be non-empty
insert into person values ('gi na', '1234');
ERROR:  login_name whitespace disallowed
insert into person values ('1gina', '1234');
ERROR:  login_name must begin with a letter.

Laten we een tandje bijsteken

Stel dat we aanvullende metadata of toepassingsgegevens willen opslaan die verband houden met de gecreëerde gebruikersrol, bijvoorbeeld misschien een tijdstempel en bron-IP-adres dat is gekoppeld aan het maken van rollen. De weergave kan natuurlijk niet aan deze nieuwe eis voldoen, aangezien er geen onderliggende opslag is, dus een echte tabel is vereist. Laten we er verder van uitgaan dat we de zichtbaarheid van die tabel willen beperken voor gebruikers die inloggen met de anonieme inlogrol. We kunnen de tabel verbergen in een aparte naamruimte (d.w.z. een PostgreSQL-schema) die ontoegankelijk blijft voor anonieme gebruikers. Laten we deze naamruimte de "private" naamruimte noemen en de tabel in de naamruimte maken:

create schema private;

create table private.person (
  login_name   name not null primary key,
  inet_client_addr inet default inet_client_addr(),
  create_time timestamptz default now()  
);

Een eenvoudig extra insert-commando binnen de triggerfunctie legt deze bijbehorende metadata vast:

create or replace function person_iit()
  returns trigger
  set schema 'public'
  language plpgsql
  security definer
  as $$
  begin

  -- Basic input sanitization
  if new.login_name is null then
    raise exception 'null login_name disallowed';
  elsif position(' ' in new.login_name) > 0 then
    raise exception 'login_name whitespace disallowed';
  elsif length(new.login_name) = 0 then
    raise exception 'login_name must be non-empty';
  elsif not (select new.login_name similar to '[A-Za-z]%') then
    raise exception 'login_name must begin with a letter.';
  end if;

  if new.login_pass is null then
    raise exception 'null login_pass disallowed';
  elsif length(new.login_pass) = 0 then
    raise exception 'login_pass must be non-empty';
  end if;

  -- Record associated metadata
  insert into private.person values (new.login_name);

  -- Provision login credentials

  execute 'create role '
    || quote_ident(new.login_name)
    || ' with login inherit nosuperuser nocreatedb nocreaterole password '
    || quote_literal(new.login_pass);

  return new;
  end;
  $$;

En we kunnen het een gemakkelijke test geven. Eerst bevestigen we dat terwijl verbonden als de anonieme rol alleen de public.person-weergave zichtbaar is en niet de private.person-tabel:

set session authorization anonymous;

\d
         List of relations
 Schema |  Name  | Type |  Owner   
--------+--------+------+----------
 public | person | view | postgres
(1 row)
                   
select * from private.person;
ERROR:  permission denied for schema private

En dan na een nieuwe rol invoegen:

insert into person values ('gina', '1234');

reset session authorization;

select * from private.person;
 login_name | inet_client_addr |          create_time          
------------+------------------+-------------------------------
 gina       | 192.168.2.106    | 2018-06-24 07:56:13.838679-07
(1 row)

de tabel private.person toont de metadata-opname voor het IP-adres en de rij-invoegtijd.

Conclusie

In dit artikel hebben we een techniek gedemonstreerd om het inrichten van PostgreSQL-rolreferenties te delegeren aan niet-superuser-rollen. Hoewel in het voorbeeld de legitimatiefunctionaliteit volledig werd gedelegeerd aan anonieme gebruikers, zou een vergelijkbare benadering kunnen worden gebruikt om de functionaliteit gedeeltelijk te delegeren aan alleen vertrouwd personeel, terwijl het voordeel behouden blijft dat dit werk wordt overgedragen aan hoogwaardig database- of systeembeheerderspersoneel. We demonstreerden ook een techniek van gelaagde gegevenstoegang met behulp van PostgreSQL-schema's, waarbij database-objecten selectief worden weergegeven of verborgen. In het volgende artikel in deze serie gaan we dieper in op de gelaagde datatoegangstechniek om een ​​nieuw databasearchitectuurontwerp voor toepassingsimplementaties voor te stellen.


  1. SQL-query om record met ID niet in een andere tabel te vinden

  2. Hoe UPPER() werkt in MariaDB

  3. Subquery gebruiken in een Check-instructie in Oracle

  4. Veilige multicloud MySQL-replicatie implementeren op AWS en GCP met VPN