PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

Samodzielne dostarczanie kont użytkowników w PostgreSQL za pośrednictwem nieuprzywilejowanego dostępu anonimowego

Notatka od Manynines:Ten blog jest publikowany pośmiertnie po śmierci Berenda Tobera 16 lipca 2018 r. Szanujemy jego wkład w społeczność PostgreSQL i życzymy pokoju naszemu przyjacielowi i gościnnemu pisarzowi.

W poprzednim artykule przedstawiliśmy podstawy wyzwalaczy PostgreSQL i funkcji składowanych oraz przedstawiliśmy sześć przykładowych przypadków użycia, w tym walidację danych, rejestrowanie zmian, wyprowadzanie wartości z wprowadzonych danych, ukrywanie danych za pomocą prostych aktualizowanych widoków, utrzymywanie danych podsumowujących w osobnych tabelach i bezpieczne wywoływanie kodu z podwyższonymi uprawnieniami. Ten artykuł opiera się na tej podstawie i przedstawia technikę wykorzystującą wyzwalacz i funkcję przechowywaną w celu ułatwienia delegowania udostępniania poświadczeń logowania do ról z ograniczonymi uprawnieniami (tj. bez uprawnień administratora). Ta funkcja może być używana do zmniejszenia obciążenia administracyjnego dla wartościowych pracowników administracyjnych systemów. Doprowadzeni do skrajności, demonstrujemy anonimowe samodzielne dostarczanie danych logowania przez użytkowników końcowych, tj. pozwalamy potencjalnym użytkownikom bazy danych na samodzielne dostarczanie danych logowania poprzez implementację „dynamicznego SQL” wewnątrz przechowywanej funkcji wykonywanej na poziomie uprawnień o odpowiednim zakresie.Wprowadzenie

Przydatne czytanie w tle

Niedawny artykuł Sebastiana Insausti na temat tego, jak zabezpieczyć bazę danych PostgreSQL, zawiera kilka bardzo istotnych wskazówek, z którymi powinieneś się zapoznać, a mianowicie porady 1–5 dotyczące kontroli uwierzytelniania klienta, konfiguracji serwera, zarządzania użytkownikami i rolami, zarządzania superużytkownikami i Szyfrowanie danych. W tym artykule wykorzystamy fragmenty każdej wskazówki.

Inny niedawny artykuł Joshuy Otwella na temat uprawnień PostgreSQL i zarządzania użytkownikami również zawiera dobre podejście do konfiguracji hosta i uprawnień użytkownika, który zawiera nieco więcej szczegółów na temat tych dwóch tematów.

Ochrona ruchu sieciowego

Proponowana funkcja obejmuje umożliwienie użytkownikom podawania danych logowania do bazy danych, a jednocześnie określają swoją nową nazwę logowania i hasło w sieci. Ochrona tej komunikacji sieciowej jest niezbędna i można ją osiągnąć, konfigurując serwer PostgreSQL tak, aby obsługiwał i wymagał szyfrowanych połączeń. Bezpieczeństwo warstwy transportowej jest włączone w pliku postgresql.conf przez ustawienie „ssl”:

ssl = on

Kontrola dostępu oparta na hoście

W tym przypadku dodamy wiersz konfiguracji dostępu opartego na hoście w pliku pg_hba.conf, który umożliwia anonimowe, tj. zaufane, logowanie do bazy danych z odpowiedniej podsieci dla populacji potencjalnych użytkowników bazy danych dosłownie przy użyciu nazwy użytkownika „anonimowy” oraz druga linia konfiguracyjna wymagająca logowania hasłem dla dowolnej innej nazwy logowania. Pamiętaj, że konfiguracje hosta wywołują pierwsze dopasowanie, więc pierwszy wiersz będzie obowiązywał zawsze, gdy zostanie określona „anonimowa” nazwa użytkownika, umożliwiając zaufane (tj. Nie wymaga hasła) połączenie, a następnie, gdy podana zostanie jakakolwiek inna nazwa użytkownika, wymagane będzie hasło. Na przykład, jeśli przykładowa baza danych „sampledb” ma być używana, powiedzmy, tylko przez pracowników i wewnętrznie w placówkach korporacyjnych, możemy skonfigurować zaufany dostęp dla pewnej nieroutowalnej podsieci wewnętrznej za pomocą:

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

Jeżeli baza danych ma być ogólnodostępna, możemy skonfigurować dostęp „na dowolny adres”:

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

Zwróć uwagę, że powyższe jest potencjalnie niebezpieczne bez dodatkowych środków ostrożności, prawdopodobnie w projekcie aplikacji lub na urządzeniu zapory, w celu ograniczenia szybkości korzystania z tej funkcji, ponieważ wiesz, że niektóre skryptowe dzieciaki zautomatyzują niekończące się tworzenie kont tylko dla lulz.

Zauważ również, że określiliśmy typ połączenia jako „hostssl”, co oznacza, że ​​połączenia nawiązywane przy użyciu protokołu TCP/IP kończą się powodzeniem tylko wtedy, gdy połączenie jest nawiązywane z szyfrowaniem SSL, aby chronić ruch sieciowy przed podsłuchiwaniem.

Blokowanie schematu publicznego

Ponieważ pozwalamy prawdopodobnie nieznanym (tj. niezaufanym) osobom na dostęp do bazy danych, będziemy chcieli mieć pewność, że dostęp domyślny jest ograniczony. Jednym z ważnych środków jest cofnięcie domyślnych uprawnień do tworzenia publicznych obiektów schematów, aby złagodzić niedawno opublikowaną lukę w zabezpieczeniach PostgreSQL związaną z domyślnymi uprawnieniami w schemacie (por. Naprawdę należy blokować schemat publiczny).

Przykładowa baza danych

Zaczniemy od pustej przykładowej bazy danych w celach ilustracyjnych:

create database sampledb;
\connect sampledb

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

Tworzymy również anonimową rolę logowania odpowiadającą wcześniejszemu ustawieniu pg_hba.conf.

create role anonymous login
    nosuperuser 
    noinherit 
    nocreatedb 
    nocreaterole 
    Noreplication;

A potem robimy coś nowatorskiego, definiując niekonwencjonalny pogląd:

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

Ten widok nie odwołuje się do tabeli, więc zapytanie wybierające zawsze zwraca pusty wiersz:

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

Jedną z rzeczy, które to dla nas robi, jest dostarczenie dokumentacji lub podpowiedzi użytkownikom końcowym, jakie dane są wymagane do założenia konta. Oznacza to, że poprzez zapytanie do tabeli, nawet jeśli wynik jest pustym wierszem, wynik ujawnia nazwy dwóch elementów danych.

Ale jeszcze lepiej, istnienie tego widoku umożliwia określenie wymaganych typów danych:

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

Będziemy wdrażać funkcję udostępniania poświadczeń z przechowywaną funkcją i wyzwalaczem, więc zadeklarujmy pusty szablon funkcji i powiązany wyzwalacz:

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

Zwróć uwagę, że postępujemy zgodnie z proponowaną konwencją nazewnictwa z poprzedniego artykułu, używając nazwy powiązanej tabeli z sufiksem skróconym oznaczającym atrybuty relacji wyzwalacza między tabelą a przechowywaną funkcją dla wyzwalacza INSTEAD OF INSERT (tj. sufiks „ iit”). Do zapisanej funkcji dodaliśmy także atrybuty SCHEMA i SECURITY DEFINER:pierwszy, ponieważ dobrą praktyką jest ustawienie ścieżki wyszukiwania, która ma zastosowanie do czasu wykonywania funkcji, a drugi, aby ułatwić tworzenie ról, które zwykle są uprawnieniami administratora bazy danych tylko, ale w tym przypadku zostaną przekazane anonimowym użytkownikom.

I na koniec dodajemy minimalnie wystarczające uprawnienia w widoku do zapytania i wstawiania:

grant select, insert on table person to anonymous;
Pobierz oficjalny dokument już dziś Zarządzanie i automatyzacja PostgreSQL za pomocą ClusterControlDowiedz się, co musisz wiedzieć, aby wdrażać, monitorować, zarządzać i skalować PostgreSQLPobierz oficjalny dokument

Przyjrzyjmy się

Przed wdrożeniem zapisanego kodu funkcji przejrzyjmy, co mamy. Najpierw jest przykładowa baza danych, której właścicielem jest użytkownik postgres:

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

Oto widok, który stworzyliśmy, oraz lista uprawnień dostępu do tworzenia i odczytu przyznanych anonimowemu użytkownikowi przez użytkownika postgres:

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

Wreszcie szczegóły tabeli pokazują nazwy kolumn i typy danych, a także powiązany wyzwalacz:

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

Dynamiczny SQL

Zamierzamy zastosować dynamiczny SQL, tj. konstruować ostateczną formę instrukcji DDL w czasie wykonywania, częściowo z danych wprowadzonych przez użytkownika, aby wypełnić treść funkcji wyzwalacza. W szczególności zakodujemy na sztywno zarys instrukcji, aby utworzyć nową rolę logowania i wypełnić określone parametry jako zmienne.

Ogólna forma tego polecenia to

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

gdzie opcja może być dowolną z szesnastu określonych właściwości. Ogólnie rzecz biorąc, wartości domyślne są odpowiednie, ale będziemy jasno określić kilka opcji ograniczających i użyjemy formularza

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

gdzie wstawimy określoną przez użytkownika nazwę roli i hasło w czasie wykonywania.

Instrukcje konstruowane dynamicznie są wywoływane za pomocą polecenia execute:

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

które dla naszych konkretnych potrzeb wyglądałyby

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

gdzie funkcja quote_literal zwraca argument ciągu znaków odpowiednio zacytowany do użycia jako literał ciągu, aby spełnić wymóg syntaktyczny, że hasło w rzeczywistości jest cytowane.

Po zbudowaniu ciągu poleceń dostarczamy go jako argument polecenia pl/pgsql execute w funkcji wyzwalacza.

Złożenie tego wszystkiego razem wygląda tak:

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

Spróbujmy!

Wszystko jest na swoim miejscu, więc dajmy się ponieść! Najpierw przełączamy autoryzację sesji na anonimowego użytkownika, a następnie wstawiamy w widoku osoby:

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

W rezultacie nowy użytkownik alice został dodany do tabeli systemowej:

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

Działa nawet bezpośrednio z wiersza poleceń systemu operacyjnego, przesyłając ciąg poleceń SQL do narzędzia klienta psql, aby dodać użytkownika 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 | {}

Załóż trochę zbroi

Pierwotny przykład funkcji wyzwalacza jest podatny na atak typu SQL injection, tj. złośliwy podmiot atakujący może sfałszować dane wejściowe, które skutkują nieautoryzowanym dostępem. Na przykład, podczas połączenia jako anonimowa rola użytkownika, próba zrobienia czegoś poza zakresem kończy się niepowodzeniem:

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

Ale następujące złośliwe dane wejściowe tworzą rolę superużytkownika o nazwie „ewa” (jak również konto wabika o nazwie „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 | {}

Następnie ukrytą rolę superużytkownika można wykorzystać do siania spustoszenia w bazie danych, na przykład usuwania kont użytkowników (lub gorzej!):

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

Aby złagodzić tę lukę, musimy podjąć kroki w celu oczyszczenia danych wejściowych. Na przykład, zastosowanie funkcji quote_ident, która zwraca ciąg znaków odpowiednio ujęty w cudzysłów do użycia jako identyfikator w instrukcji SQL z dodanymi cudzysłowami, gdy jest to konieczne, na przykład jeśli ciąg zawiera znaki niebędące identyfikatorami lub byłby złożony z liter i prawidłowo osadzony cytaty:

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

Teraz, jeśli ten sam exploit SQL injection próbuje stworzyć innego superużytkownika o nazwie „frank”, kończy się to niepowodzeniem, a wynikiem jest bardzo niekonwencjonalna nazwa użytkownika:

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

Możemy zastosować dalszą sensowną walidację danych w funkcji wyzwalacza, na przykład wymaganie tylko alfanumerycznych nazw użytkownika i odrzucanie spacji i innych znaków:

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

a następnie potwierdź, że różne kontrole dezynfekcji działają:

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.

Podnieśmy poziom

Załóżmy, że chcemy przechowywać dodatkowe metadane lub dane aplikacji związane z utworzoną rolą użytkownika, np. być może znacznik czasu i źródłowy adres IP powiązany z tworzeniem roli. Widok oczywiście nie może spełnić tego nowego wymagania, ponieważ nie ma podstawowej pamięci masowej, więc wymagana jest rzeczywista tabela. Załóżmy również, że chcemy ograniczyć widoczność tej tabeli dla użytkowników logujących się z rolą anonimowego logowania. Możemy ukryć tabelę w oddzielnej przestrzeni nazw (tj. schemacie PostgreSQL), która pozostaje niedostępna dla anonimowych użytkowników. Nazwijmy tę przestrzeń nazw „prywatną” przestrzenią nazw i utwórzmy tabelę w przestrzeni nazw:

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

Proste dodatkowe polecenie wstawiania wewnątrz funkcji wyzwalacza rejestruje te powiązane metadane:

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

I możemy dać to łatwy test. Najpierw potwierdzamy, że podczas połączenia jako anonimowa rola widoczny jest tylko widok public.person, a nie tabela private.person:

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

A potem po wstawieniu nowej roli:

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)

tabela private.person pokazuje przechwytywanie metadanych dla adresu IP i czasu wstawienia wiersza.

Wniosek

W tym artykule przedstawiliśmy technikę delegowania udostępniania poświadczeń roli PostgreSQL do ról niebędących superużytkownikami. Chociaż w przykładzie w pełni delegowano funkcję uwierzytelnienia do anonimowych użytkowników, podobne podejście można zastosować do częściowego delegowania funkcji tylko do zaufanego personelu, przy jednoczesnym zachowaniu korzyści z odciążenia tej pracy od wartościowych pracowników baz danych lub administratorów systemów. Zademonstrowaliśmy również technikę warstwowego dostępu do danych wykorzystującą schematy PostgreSQL, selektywnie eksponując lub ukrywając obiekty bazy danych. W następnym artykule z tej serii omówimy technikę warstwowego dostępu do danych, aby zaproponować nowatorski projekt architektury bazy danych dla implementacji aplikacji.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Plik ~/.psqlrc dla DBA

  2. SQL — Utwórz widok z wielu tabel

  3. Jak pracować z PGpoint dla Geolokalizacji przy użyciu PostgreSQL?

  4. Jak wyeksportować tabelę jako CSV z nagłówkami na Postgresql?

  5. Jak porównywać tablice w PostgreSQL