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.