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

Użytkownicy aplikacji a zabezpieczenia na poziomie wiersza

Kilka dni temu pisałem na blogu o typowych problemach z rolami i uprawnieniami, które odkrywamy podczas przeglądów bezpieczeństwa.

Oczywiście PostgreSQL oferuje wiele zaawansowanych funkcji związanych z bezpieczeństwem, jedną z nich jest Row Level Security (RLS), dostępny od wersji PostgreSQL 9.5.

Ponieważ wersja 9.5 została wydana w styczniu 2016 (a więc zaledwie kilka miesięcy temu), RLS jest dość nową funkcją i tak naprawdę nie mamy jeszcze do czynienia z wieloma wdrożeniami produkcyjnymi. Zamiast tego RLS jest powszechnym tematem dyskusji na temat „jak wdrożyć”, a jednym z najczęstszych pytań jest to, jak sprawić, by działał z użytkownikami na poziomie aplikacji. Zobaczmy więc, jakie są możliwe rozwiązania.

Wprowadzenie do RLS

Zobaczmy najpierw bardzo prosty przykład, wyjaśniający, o co chodzi w RLS. Załóżmy, że prowadzimy chat tabela przechowująca wiadomości wysyłane między użytkownikami – użytkownicy mogą wstawiać do niej wiersze, aby wysyłać wiadomości do innych użytkowników, a także wysyłać do niej zapytania, aby zobaczyć wiadomości wysłane do nich przez innych użytkowników. Tabela może więc wyglądać tak:

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_temject VARCHAR(64) NOT NULL,<_body TEXT); /pre> 

Klasyczne zabezpieczenia oparte na rolach pozwalają nam ograniczyć dostęp tylko do całej tabeli lub jej pionowych wycinków (kolumn). Dlatego nie możemy go użyć, aby uniemożliwić użytkownikom czytanie wiadomości przeznaczonych dla innych użytkowników lub wysyłanie wiadomości z fałszywym message_from pole.

I właśnie do tego służy RLS – pozwala tworzyć reguły (polityki) ograniczające dostęp do podzbiorów wierszy. Na przykład możesz to zrobić:

UTWÓRZ POLITYKĘ chat_policy NA czacie UŻYWAJĄC ((message_to =current_user) LUB (message_from =current_user)) Z CZEKIEM (message_from =current_user)

Ta zasada zapewnia, że ​​użytkownik może zobaczyć tylko wiadomości wysłane przez niego lub przeznaczone dla niego – taki jest warunek w USING klauzula. Druga część zasad (WITH CHECK ) zapewnia, że ​​użytkownik może wstawiać wiadomości tylko ze swoją nazwą użytkownika w message_from kolumna, zapobiegając wiadomościom z fałszywym nadawcą.

Możesz również wyobrazić sobie RLS jako automatyczny sposób dołączania dodatkowych warunków WHERE. Możesz to zrobić ręcznie na poziomie aplikacji (a wcześniej ludzie RLS często to robili), ale RLS robi to w sposób niezawodny i bezpieczny (na przykład wiele wysiłku włożono w zapobieganie różnym wyciekom informacji).

Uwaga :Przed RLS popularnym sposobem osiągnięcia czegoś podobnego było bezpośrednie uniemożliwienie dostępu do tabeli (odwołanie wszystkich uprawnień) i udostępnienie zestawu funkcji definiujących zabezpieczenia, aby uzyskać do niej dostęp. Osiągnęło to w większości ten sam cel, ale funkcje mają różne wady – mają tendencję do mylenia optymalizatora i poważnie ograniczają elastyczność (jeśli użytkownik musi coś zrobić, a nie ma do tego odpowiedniej funkcji, ma pecha). I oczywiście musisz napisać te funkcje.

Użytkownicy aplikacji

Jeśli przeczytasz oficjalną dokumentację dotyczącą RLS, możesz zauważyć jeden szczegół – wszystkie przykłady używają current_user , czyli aktualny użytkownik bazy danych. Ale nie tak działa obecnie większość aplikacji bazodanowych. Aplikacje internetowe z wieloma zarejestrowanymi użytkownikami nie obsługują mapowania 1:1 na użytkowników bazy danych, ale zamiast tego używają pojedynczego użytkownika bazy danych do samodzielnego uruchamiania zapytań i zarządzania użytkownikami aplikacji — być może w grupie users tabela.

Technicznie nie jest problemem tworzenie wielu użytkowników baz danych w PostgreSQL. Baza danych powinna sobie z tym poradzić bez żadnych problemów, ale aplikacje tego nie robią z wielu praktycznych powodów. Na przykład muszą śledzić dodatkowe informacje dla każdego użytkownika (np. dział, stanowisko w organizacji, dane kontaktowe, …), więc aplikacja będzie potrzebowała users stół mimo wszystko.

Innym powodem może być łączenie połączeń – przy użyciu jednego współdzielonego konta użytkownika, chociaż wiemy, że można to rozwiązać za pomocą dziedziczenia i SET ROLE (patrz poprzedni post).

Załóżmy jednak, że nie chcesz tworzyć oddzielnych użytkowników bazy danych — chcesz nadal korzystać z jednego udostępnionego konta bazy danych i używać RLS z użytkownikami aplikacji. Jak to zrobić?

Zmienne sesji

Zasadniczo potrzebujemy przekazać dodatkowy kontekst do sesji bazy danych, abyśmy mogli później użyć go z polityki bezpieczeństwa (zamiast current_user zmienny). A najłatwiejszym sposobem na zrobienie tego w PostgreSQL są zmienne sesji:

USTAW my.username ='tomas'

Jeśli przypomina to zwykłe parametry konfiguracyjne (np. SET work_mem = '...' ), masz całkowitą rację – to w większości to samo. Polecenie definiuje nową przestrzeń nazw (my ) i dodaje username zmienna do niego. Nowa przestrzeń nazw jest wymagana, ponieważ globalna jest zarezerwowana dla konfiguracji serwera i nie możemy dodawać do niej nowych zmiennych. To pozwala nam zmienić politykę bezpieczeństwa w następujący sposób:

UTWÓRZ POLITYKĘ chat_policy NA czacie UŻYWAJĄC (current_setting('my.username') IN (message_from, message_to)) Z CZEKIEM (message_from =current_setting('my.username'))

Wszystko, co musimy zrobić, to upewnić się, że pula połączeń / aplikacja ustawia nazwę użytkownika za każdym razem, gdy uzyskuje nowe połączenie i przypisuje ją do zadania użytkownika.

Pozwolę sobie zaznaczyć, że to podejście załamuje się, gdy pozwolisz użytkownikom na uruchamianie dowolnego kodu SQL na połączeniu lub jeśli użytkownik zdoła odkryć odpowiednią lukę w postaci wstrzyknięcia SQL. W takim przypadku nic nie może ich powstrzymać przed ustawieniem dowolnej nazwy użytkownika. Ale nie rozpaczaj, istnieje wiele rozwiązań tego problemu, a my szybko je omówimy.

Podpisane zmienne sesji

Pierwsze rozwiązanie to proste ulepszenie zmiennych sesji – tak naprawdę nie możemy uniemożliwić użytkownikom ustawienia dowolnej wartości, ale co by było, gdybyśmy mogli zweryfikować, czy wartość nie została obalona? Jest to dość łatwe do zrobienia za pomocą prostego podpisu cyfrowego. Zamiast przechowywać nazwę użytkownika, zaufana część (pula połączeń, aplikacja) może zrobić coś takiego:

podpis =sha256(nazwa użytkownika + znacznik czasu + TAJNE)

a następnie zapisz wartość i podpis w zmiennej sesji:

USTAW my.username ='nazwa użytkownika:znacznik czasu:podpis'

Zakładając, że użytkownik nie zna ciągu SECRET (np. 128B losowych danych), modyfikacja wartości nie powinna być możliwa bez unieważnienia podpisu.

Uwaga :To nie jest nowy pomysł – to w zasadzie to samo, co podpisane pliki cookie HTTP. Django ma całkiem niezłą dokumentację na ten temat.

Najłatwiejszym sposobem ochrony wartości SECRET jest przechowywanie jej w tabeli niedostępnej dla użytkownika i dostarczanie security definer funkcja wymagająca hasła (aby użytkownik nie mógł po prostu podpisać dowolnych wartości).

CREATE FUNCTION set_username(uname TEXT, pwd TEXT) ZWRACA tekst AS $DECLARE v_key TEXT; v_value TEXT;BEGIN SELECT sign_key INTO v_key FROM wpisów tajnych; v_value :=unazwa || ':' || ekstrakt(epoka od teraz())::int; v_value :=v_value || ':' || crypt(v_value || ':' || v_key, gen_salt('bf')); WYKONAJ set_config('moja.nazwa użytkownika', v_value, false); RETURN v_value;END;$ JĘZYK plpgsql SECURITY DEFINER STABILNY;

Funkcja po prostu wyszukuje klucz podpisywania (sekret) w tabeli, oblicza podpis, a następnie ustawia wartość w zmiennej sesji. Zwraca również wartość, głównie dla wygody.

Więc zaufana część może to zrobić przed przekazaniem połączenia użytkownikowi (oczywiście „hasło” nie jest zbyt dobrym hasłem do produkcji):

SELECT set_username('tomas', 'passphrase')

A potem oczywiście potrzebujemy innej funkcji, która po prostu weryfikuje podpis i albo wyświetla błędy, albo zwraca nazwę użytkownika, jeśli podpis pasuje.

CREATE FUNCTION get_username() ZWRACA tekst JAKO $DECLARE v_key TEXT; v_parts TEKST[]; v_uname TEKST; v_value TEKST; v_sygnatura czasowa INT; v_signature TEXT;BEGIN — tym razem bez weryfikacji hasła SELECT sign_key INTO v_key FROM sekretów; 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_value :=v_uname || ':' || v_sygnatura czasowa || ':' || v_klucz; JEŻELI v_signature =crypt(v_value, v_signature) TO ZWRÓĆ v_uname; KONIEC JEŚLI; RAISE EXCEPTION 'nieprawidłowa nazwa użytkownika / znacznik czasu';END;$ LANGUAGE plpgsql SECURITY DEFINER STABLE;

A ponieważ ta funkcja nie wymaga hasła, użytkownik może po prostu zrobić to:

SELECT get_username()

Ale get_username() funkcja przeznaczona dla polityk bezpieczeństwa, m.in. tak:

UTWÓRZ ZASADY chat_policy NA czacie UŻYWAJĄC (get_username() IN (message_from, message_to)) Z CZEKIEM (message_from =get_username())

Pełniejszy przykład, spakowany jako proste rozszerzenie, można znaleźć tutaj.

Zwróć uwagę, że wszystkie obiekty (tabele i funkcje) są własnością uprzywilejowanego użytkownika, a nie użytkownika uzyskującego dostęp do bazy danych. Użytkownik ma tylko EXECUTE uprawnienia do funkcji, które są jednak zdefiniowane jako SECURITY DEFINER . To właśnie sprawia, że ​​ten schemat działa, jednocześnie chroniąc sekret przed użytkownikiem. Funkcje są zdefiniowane jako STABLE , aby ograniczyć liczbę wywołań do crypt() funkcja (która jest celowo kosztowna, aby zapobiec bruteforsingowi).

Przykładowe funkcje zdecydowanie wymagają więcej pracy. Ale miejmy nadzieję, że wystarczy, aby weryfikacja koncepcji demonstrowała, jak przechowywać dodatkowy kontekst w chronionej zmiennej sesji.

Pytasz, co należy naprawić? Po pierwsze, funkcje niezbyt dobrze radzą sobie z różnymi sytuacjami błędów. Po drugie, chociaż podpisana wartość zawiera znacznik czasu, tak naprawdę nic z tym nie robimy – może to być na przykład użyte do wygaśnięcia wartości. Do wartości można dodać dodatkowe bity, np. dział użytkownika, a nawet informacje o sesji (np. PID procesu zaplecza, aby zapobiec ponownemu wykorzystaniu tej samej wartości na innych połączeniach).

Krypto

Te dwie funkcje opierają się na kryptografii – nie używamy zbyt wiele, z wyjątkiem kilku prostych funkcji mieszających, ale nadal jest to prosty schemat kryptograficzny. I wszyscy wiedzą, że nie powinieneś robić własnego krypto. Dlatego użyłem rozszerzenia pgcrypto, w szczególności crypt() funkcji, aby obejść ten problem. Ale nie jestem kryptografem, więc chociaż uważam, że cały schemat jest w porządku, może czegoś brakuje – daj mi znać, jeśli coś zauważysz.

Ponadto podpisywanie byłoby świetnym dopasowaniem do kryptografii z kluczem publicznym – moglibyśmy użyć zwykłego klucza PGP z hasłem do podpisywania, a części publicznej do weryfikacji podpisu. Niestety, chociaż pgcrypto obsługuje szyfrowanie PGP, nie obsługuje podpisywania.

Alternatywne podejścia

Oczywiście istnieją różne alternatywne rozwiązania. Na przykład zamiast przechowywać tajny klucz podpisu w tabeli, możesz go na stałe zakodować w funkcji (ale wtedy musisz upewnić się, że użytkownik nie widzi kodu źródłowego). Możesz też wykonać podpisywanie w funkcji C, w którym to przypadku jest ona ukryta przed wszystkimi, którzy nie mają dostępu do pamięci (w takim przypadku i tak przegrałeś).

Ponadto, jeśli w ogóle nie podoba ci się podejście do podpisywania, możesz zastąpić podpisaną zmienną bardziej tradycyjnym rozwiązaniem „skarbca”. Potrzebujemy sposobu na przechowywanie danych, ale musimy upewnić się, że użytkownik nie może dowolnie przeglądać ani modyfikować zawartości, z wyjątkiem określonego sposobu. Ale hej, to właśnie zwykłe tabele z interfejsem API zaimplementowanym przy użyciu security definer funkcje mogą zrobić!

Nie będę tutaj przedstawiał całego przerobionego przykładu (sprawdź to rozszerzenie, aby uzyskać pełny przykład), ale potrzebujemy sessions stół pełniący rolę skarbca:

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

Tabela nie może być dostępna dla zwykłych użytkowników bazy danych – proste REVOKE ALL FROM ... powinien się tym zająć. A potem API składające się z dwóch głównych funkcji:

  • set_username(user_name, passphrase) – generuje losowy UUID, wstawia dane do sejfu i przechowuje UUID w zmiennej sesji
  • get_username() – odczytuje UUID ze zmiennej sesji i wyszukuje wiersz w tabeli (błędy w przypadku braku pasującego wiersza)

Takie podejście zastępuje ochronę podpisu losowością UUID – użytkownik może dostosować zmienną sesji, ale prawdopodobieństwo trafienia istniejącego identyfikatora jest znikome (UUID to 128-bitowe wartości losowe).

Jest to nieco bardziej tradycyjne podejście, opierające się na tradycyjnych zabezpieczeniach opartych na rolach, ale ma też kilka wad – na przykład faktycznie zapisuje bazy danych, co oznacza, że ​​jest z natury niekompatybilne z systemami gorącej rezerwy.

Pozbywanie się hasła

Możliwe jest również zaprojektowanie sejfu tak, aby hasło nie było potrzebne. Wprowadziliśmy go, ponieważ założyliśmy set_username dzieje się na tym samym połączeniu – musimy zachować funkcję wykonywalną (więc mieszanie się z rolami lub uprawnieniami nie jest rozwiązaniem), a hasło zapewnia, że ​​tylko zaufany komponent może z niego korzystać.

Ale co, jeśli podpisywanie/tworzenie sesji odbywa się na osobnym połączeniu, a tylko wynik (podpisana wartość lub identyfikator sesji UUID) jest kopiowany do połączenia przekazanego użytkownikowi? Cóż, w takim razie nie potrzebujemy już hasła. (To trochę podobne do tego, co robi Kerberos – generowanie biletu na zaufanym połączeniu, a następnie używanie biletu do innych usług.)

Podsumowanie

Pozwolę sobie więc szybko podsumować ten wpis na blogu:

  • Podczas gdy wszystkie przykłady RLS używają użytkowników bazy danych (za pomocą current_user ), nie jest trudno sprawić, by RLS działał z użytkownikami aplikacji.
  • Zmienne sesji są niezawodnym i dość prostym rozwiązaniem, zakładając, że system ma zaufany komponent, który może ustawić zmienną przed przekazaniem połączenia użytkownikowi.
  • Kiedy użytkownik może wykonać dowolny kod SQL (zarówno zgodnie z projektem, jak i dzięki luce), podpisana zmienna uniemożliwia użytkownikowi zmianę wartości.
  • Możliwe są inne rozwiązania, m.in. zastąpienie zmiennych sesji tabelą przechowującą informacje o sesjach zidentyfikowanych przez losowy UUID.
  • Dobrą rzeczą jest to, że zmienne sesji nie zapisują bazy danych, więc to podejście może działać w systemach tylko do odczytu (np. Hot Standby).

W następnej części tej serii blogów przyjrzymy się używaniu użytkowników aplikacji, gdy system nie ma zaufanego komponentu (więc nie może ustawić zmiennej sesji lub utworzyć wiersza w sessions tabeli) lub gdy chcemy przeprowadzić (dodatkowe) niestandardowe uwierzytelnianie w bazie danych.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Utwórz bazę danych Postgres za pomocą Pythona

  2. Postgresql - tworzenie kopii zapasowej bazy danych i przywracanie na innego właściciela?

  3. Hibernacja i wielodostępna baza danych przy użyciu schematów w PostgreSQL

  4. Nieudane koło budowania dla psycopg2 - MacOSX przy użyciu virtualenv i pip

  5. Jak znaleźć odstęp między dwiema datami w PostgreSQL