sql >> Databasteknik >  >> RDS >> PostgreSQL

Självtillhandahållande av användarkonton i PostgreSQL via oprivilegierad anonym åtkomst

Anteckning från Severalnines:Den här bloggen publiceras postumt när Berend Tober gick bort den 16 juli 2018. Vi hedrar hans bidrag till PostgreSQL-communityt och önskar fred för vår vän och gästskribent.

I tidigare artikel introducerade vi grunderna för PostgreSQL-utlösare och lagrade funktioner och gav sex exempel på användningsfall inklusive datavalidering, ändringsloggning, härledning av värden från infogade data, datadöljning med enkla uppdateringsbara vyer, underhåll av sammanfattningsdata i separata tabeller och säker anrop av kod med förhöjd behörighet. Den här artikeln bygger vidare på den grunden och presenterar en teknik som använder en utlösare och lagrad funktion för att underlätta delegering av inloggningsuppgifter till roller med begränsade privilegier (dvs. icke-superanvändare). Den här funktionen kan användas för att minska den administrativa arbetsbelastningen för högt värdefulla systemadministratörer. Till det yttersta demonstrerar vi anonym slutanvändares självtillhandahållande av inloggningsuppgifter, d.v.s. låter potentiella databasanvändare tillhandahålla inloggningsuppgifter på egen hand genom att implementera "dynamisk SQL" i en lagrad funktion som körs på behörighetsnivå med lämplig omfattning. Inledning

Användbar bakgrundsläsning

Den senaste artikeln av Sebastian Insausti om hur du säkrar din PostgreSQL-databas innehåller några mycket relevanta tips som du bör känna till, nämligen tips #1 - #5 om klientautentiseringskontroll, serverkonfiguration, användar- och rollhantering, superanvändarhantering och Datakryptering. Vi kommer att använda delar av varje tips i den här artikeln.

En annan artikel nyligen av Joshua Otwell om PostgreSQL Privileges &User Management har också en bra behandling av värdkonfiguration och användarprivilegier som går in lite mer i detalj på dessa två ämnen.

Skydda nätverkstrafik

Den föreslagna funktionen innebär att användarna kan tillhandahålla inloggningsuppgifter för databasen och medan de gör det kommer de att ange sitt nya inloggningsnamn och lösenord över nätverket. Skydd av denna nätverkskommunikation är väsentligt och kan uppnås genom att konfigurera PostgreSQL-servern för att stödja och kräva krypterade anslutningar. Transportlagersäkerheten är aktiverad i postgresql.conf-filen med inställningen "ssl":

ssl = on

Värdbaserad åtkomstkontroll

I det aktuella fallet kommer vi att lägga till en värdbaserad åtkomstkonfigurationsrad i filen pg_hba.conf som tillåter anonym, d.v.s. pålitlig, inloggning till databasen från något lämpligt undernätverk för populationen av potentiella databasanvändare som bokstavligen använder användarnamnet "anonym" och en andra konfigurationsrad som kräver lösenordsinloggning för alla andra inloggningsnamn. Kom ihåg att värdkonfigurationer anropar den första matchningen, så den första raden kommer att gälla närhelst det "anonyma" användarnamnet anges, vilket tillåter en pålitlig (dvs inget lösenord krävs) anslutning, och sedan, när något annat användarnamn anges, kommer ett lösenord att krävas. Om exempeldatabasen "sampledb" till exempel endast ska användas av anställda och internt på företagsanläggningar, kan vi konfigurera betrodd åtkomst för något icke-routbart internt subnät med:

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

Om databasen ska göras allmänt tillgänglig för allmänheten, kan vi konfigurera "valfri adress"-åtkomst:

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

Notera att ovanstående är potentiellt farligt utan ytterligare försiktighetsåtgärder, möjligen i applikationsdesignen eller på en brandväggsenhet, för att hastighetsbegränsa användningen av den här funktionen, eftersom du vet att någon scriptkiddie kommer att automatisera oändligt kontoskapande bara för lulz.

Observera också att vi har angett anslutningstypen som "hostssl", vilket betyder att anslutningar som görs med TCP/IP bara lyckas när anslutningen görs med SSL-kryptering för att skydda nätverkstrafiken från avlyssning.

Låsa ner det offentliga schemat

Eftersom vi tillåter möjligen okända (d.v.s. opålitliga) personer att komma åt databasen, vill vi vara säkra på att standardåtkomster är begränsade. En viktig åtgärd är att återkalla privilegiet för att skapa standardobjekt för offentliga scheman för att mildra en nyligen publicerad PostgreSQL-sårbarhet relaterad till standardschemaprivilegier (jfr. Locking Down the Public Schema by yours truly).

En exempeldatabas

Vi börjar med en tom exempeldatabas i illustrationssyfte:

create database sampledb;
\connect sampledb

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

Vi skapar också den anonyma inloggningsrollen som motsvarar den tidigare inställningen pg_hba.conf.

create role anonymous login
    nosuperuser 
    noinherit 
    nocreatedb 
    nocreaterole 
    Noreplication;

Och så gör vi något nytt genom att definiera en okonventionell syn:

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

Den här vyn refererar till ingen tabell och därför returnerar en urvalsfråga alltid en tom rad:

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

En sak detta gör för oss är på sätt och vis att tillhandahålla dokumentation eller en ledtråd till slutanvändare om vilken data som krävs för att upprätta ett konto. Det vill säga, genom att fråga tabellen, även om resultatet är en tom rad, avslöjar resultatet namnen på de två dataelementen.

Men ännu bättre, förekomsten av denna vy tillåter bestämning av de datatyper som krävs:

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

Vi kommer att implementera funktionen för tillhandahållande av autentiseringsuppgifter med en lagrad funktion och utlösare, så låt oss deklarera en tom funktionsmall och den tillhörande utlösaren:

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();

Observera att vi följer den föreslagna namnkonventionen från föregående artikel och använder det associerade tabellnamnet suffixat med en förkortning som anger attribut för triggerrelationen mellan tabellen och den lagrade funktionen för en INSTEAD OF INSERT-utlösare (d.v.s. suffixet " jag det"). Vi har också lagt till attributen SCHEMA och SECURITY DEFINER till den lagrade funktionen:det förra eftersom det är god praxis att ställa in sökvägen som gäller för varaktigheten av funktionskörning, och det senare för att underlätta rollskapandet, vilket normalt är en databas superanvändarbehörighet endast men i detta fall kommer att delegeras till anonyma användare.

Och slutligen lägger vi till minimalt tillräckliga behörigheter för vyn för att fråga och infoga:

grant select, insert on table person to anonymous;
Ladda ner Whitepaper Today PostgreSQL Management &Automation med ClusterControlLäs om vad du behöver veta för att distribuera, övervaka, hantera och skala PostgreSQLDladda Whitepaper

Låt oss granska

Innan vi implementerar den lagrade funktionskoden, låt oss se över vad vi har. Först finns det exempeldatabasen som ägs av postgres-användaren:

\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 | {}

Och det är vyn vi skapade och en lista över skapande och läsrättigheter som beviljats ​​den anonyma användaren av postgres-användaren:

\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)

Slutligen visar tabelldetaljen kolumnnamnen och datatyperna samt tillhörande utlösare:

\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()

Dynamisk SQL

Vi kommer att använda dynamisk SQL, d.v.s. konstruera den slutliga formen av en DDL-sats vid körning, delvis från användarinmatade data, för att fylla i triggerfunktionskroppen. Specifikt hårdkodar vi konturerna av uttalandet för att skapa en ny inloggningsroll och fylla i de specifika parametrarna som variabler.

Den allmänna formen för detta kommando är

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

där alternativ kan vara vilken som helst av sexton specifika egenskaper. I allmänhet är standardinställningarna lämpliga men vi kommer att vara tydliga om flera begränsningsalternativ och använda formuläret

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

där vi kommer att infoga det användarspecificerade rollnamnet och lösenordet vid körning.

Dynamiskt konstruerade satser anropas med kommandot execute:

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

som för våra specifika behov skulle se ut

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

där funktionen quote_literal returnerar strängargumentet som är lämpligt citerat för att användas som en strängliteral för att uppfylla det syntaktiska kravet att lösenordet faktiskt ska citeras.

När vi har byggt kommandosträngen, tillhandahåller vi den som argument till pl/pgsql exekveringskommandot i triggerfunktionen.

Att sätta ihop det hela ser ut så här:

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;
  $$;

Låt oss prova!

Allt är på plats, så låt oss snurra! Först byter vi sessionsauktorisering till den anonyma användaren och gör sedan en infogning mot personvyn:

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

Resultatet är att ny användare alice har lagts till i systemtabellen:

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

Det fungerar till och med direkt från operativsystemets kommandorad genom att skicka en SQL-kommandosträng till psql-klientverktyget för att lägga till användaren bob:

$ 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 | {}

Applicera lite rustning

Det initiala exemplet på triggerfunktionen är sårbart för SQL-injektionsattacker, det vill säga en skadlig hotaktör kan skapa indata som resulterar i obehörig åtkomst. Till exempel, när ett försök att göra något utanför räckvidden misslyckas som den anonyma användarrollen:

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

Men följande skadliga indata skapar en superanvändarroll som heter 'eve' (liksom ett lockbetekonto som heter '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 | {}

Sedan kan den smygande superanvändarrollen användas för att skapa kaos i databasen, till exempel att ta bort användarkonton (eller ännu värre!):

\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 | {}

För att mildra denna sårbarhet måste vi vidta åtgärder för att sanera indata. Till exempel att tillämpa funktionen quote_ident, som returnerar en sträng som är lämpligt citerad för användning som en identifierare i en SQL-sats med citattecken som läggs till när det behövs, till exempel om strängen innehåller icke-identifierande tecken eller skulle vara skiftlägesvikt och korrekt dubblering inbäddad citat:

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;
  $$;

Om nu samma SQL-injektion utnyttjas för att skapa en annan superanvändare som heter "frank", misslyckas det, och resultatet är ett mycket oortodoxt användarnamn:

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 | {}

Vi kan tillämpa ytterligare förnuftig datavalidering inom triggerfunktionen, som att endast kräva alfanumeriska användarnamn och avvisa blanksteg och andra tecken:

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;
  $$;

och bekräfta sedan att de olika saneringskontrollerna fungerar:

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.

Låt oss öka ett snäpp

Anta att vi vill lagra ytterligare metadata eller applikationsdata relaterat till den skapade användarrollen, t.ex. kanske en tidsstämpel och källans IP-adress som är kopplad till rollskapandet. Utsikten kan naturligtvis inte tillfredsställa detta nya krav eftersom det inte finns någon underliggande lagring, så en faktisk tabell krävs. Låt oss också anta att vi vill begränsa synligheten för den tabellen från användare som loggar in med den anonyma inloggningsrollen. Vi kan gömma tabellen i ett separat namnutrymme (dvs ett PostgreSQL-schema) som förblir otillgängligt för anonyma användare. Låt oss kalla detta namnutrymme för det "privata" namnområdet och skapa tabellen i namnområdet:

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()  
);

Ett enkelt ytterligare infogningskommando inuti triggerfunktionen registrerar denna associerade metadata:

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;
  $$;

Och vi kan ge det ett enkelt test. Först bekräftar vi att medan den är ansluten som den anonyma rollen är endast public.person-vyn synlig och inte private.person-tabellen:

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

Och sedan efter en ny roll infoga:

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)

tabellen private.person visar metadatainsamlingen för IP-adressen och tiden för radinsättning.

Slutsats

I den här artikeln har vi visat en teknik för att delegera postgreSQL-rolluppgifter till roller som inte är superanvändare. Även om exemplet helt delegerade legitimationsfunktionen till anonyma användare, kunde ett liknande tillvägagångssätt användas för att delvis delegera funktionen till endast betrodd personal samtidigt som man behåller fördelen med att avlasta detta arbete från värdefull databas- eller systemadministratörspersonal. Vi demonstrerade också en teknik för skiktad dataåtkomst med hjälp av PostgreSQL-scheman, selektivt exponera eller dölja databasobjekt. I nästa artikel i den här serien kommer vi att utvidga tekniken för åtkomst av data i lager för att föreslå en ny databasarkitekturdesign för tillämpningar.


  1. Kan inte ansluta till MySQL från Java:NullPointerException inuti MySQL-drivrutinanslutningslogiken

  2. Kombinera kapslade loop-frågor till överordnat arrayresultat - pg-promise

  3. PLSQL Infoga i med subquery och returnerande klausul

  4. RMAN Lista backup-kommandon