Před několika dny jsem psal na blogu o běžných problémech s rolemi a oprávněními, které objevujeme při kontrolách zabezpečení.
PostgreSQL samozřejmě nabízí mnoho pokročilých funkcí souvisejících se zabezpečením, jednou z nich je Row Level Security (RLS), dostupné od PostgreSQL 9.5.
Vzhledem k tomu, že verze 9.5 byla vydána v lednu 2016 (tedy jen před několika měsíci), RLS je poměrně nová funkce a ve skutečnosti se zatím nezabýváme mnoha produkčními nasazeními. Místo toho je RLS běžným předmětem diskusí „jak implementovat“ a jednou z nejčastějších otázek je, jak zajistit, aby to fungovalo s uživateli na úrovni aplikace. Pojďme se tedy podívat, jaká možná řešení existují.
Úvod do RLS
Podívejme se nejprve na velmi jednoduchý příklad, který vysvětluje, o čem RLS je. Řekněme, že máme chat
tabulka ukládající zprávy odeslané mezi uživateli – uživatelé do ní mohou vkládat řádky pro odesílání zpráv dalším uživatelům a dotazovat se, aby viděli zprávy, které jim odesílají jiní uživatelé. Tabulka tedy může vypadat takto:
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_subject VARCHAR(64) message NOT TEXT); /před>Klasické zabezpečení založené na rolích nám umožňuje omezit přístup buď k celé tabulce, nebo k jejím vertikálním výřezům (sloupcům). Nemůžeme jej tedy použít k tomu, abychom uživatelům zabránili ve čtení zpráv určených pro jiné uživatele nebo odesílání zpráv s falešným
message_from
pole.A přesně k tomu slouží RLS – umožňuje vám vytvářet pravidla (zásady) omezující přístup k podmnožinám řádků. Můžete to udělat například takto:
VYTVOŘTE ZÁSADY chat_policy NA chatu POMOCÍ ((message_to =aktuální_uživatel) NEBO (message_from =aktuální_uživatel)) SE KONTROLOU (message_from =aktuální_uživatel)Tato zásada zajišťuje, že uživatel může vidět pouze zprávy, které odeslal nebo které mu byly určeny – to je podmínka v
USING
doložka ano. Druhá část zásady (WITH CHECK
) zajišťuje, že uživatel může domessage_from
vkládat pouze zprávy se svým uživatelským jménem sloupec, který zabraňuje zprávám s podvrženým odesílatelem.RLS si také můžete představit jako automatický způsob připojení dalších podmínek WHERE. Dalo by se to udělat ručně na úrovni aplikace (a před RLS to lidé často dělali), ale RLS to dělá spolehlivým a bezpečným způsobem (velké úsilí bylo vynaloženo například na zabránění různým únikům informací).
Poznámka :Před RLS bylo oblíbeným způsobem, jak dosáhnout něčeho podobného, přímé znepřístupnění tabulky (zrušení všech oprávnění) a poskytnutí sady funkcí definujících zabezpečení pro přístup k ní. Tím bylo dosaženo většinou stejného cíle, ale funkce mají různé nevýhody – mají tendenci zmást optimalizátor a vážně omezují flexibilitu (pokud uživatel potřebuje něco udělat a není pro to vhodná funkce, má smůlu). A samozřejmě musíte tyto funkce napsat.
Uživatelé aplikace
Pokud si přečtete oficiální dokumentaci o RLS, můžete si všimnout jednoho detailu – všechny příklady používají
current_user
, tedy aktuální uživatel databáze. Ale takto většina databázových aplikací v dnešní době nefunguje. Webové aplikace s mnoha registrovanými uživateli neudržují mapování 1:1 na databázové uživatele, ale místo toho používají jednoho databázového uživatele ke spouštění dotazů a správě uživatelů aplikací samy – možná vusers
tabulka.Technicky není problém vytvořit v PostgreSQL mnoho databázových uživatelů. Databáze by to měla zvládnout bez problémů, ale aplikace to z řady praktických důvodů nedělají. Potřebují například sledovat další informace pro každého uživatele (např. oddělení, pozici v organizaci, kontaktní údaje, …), takže aplikace bude potřebovat
users
stejně stůl.Dalším důvodem může být sdružování připojení – použití jednoho sdíleného uživatelského účtu, i když víme, že je to řešitelné pomocí dědičnosti a
SET ROLE
(viz předchozí příspěvek).Předpokládejme však, že nechcete vytvářet samostatné uživatele databáze – chcete nadále používat jeden sdílený databázový účet a používat RLS s uživateli aplikace. Jak to udělat?
Proměnné relace
V podstatě to, co potřebujeme, je předat relaci databáze další kontext, abychom jej mohli později použít z bezpečnostní politiky (namísto
current_user
proměnná). A nejjednodušší způsob, jak toho dosáhnout v PostgreSQL, jsou proměnné session:SET my.username ='tomas'Pokud se to podobá obvyklým konfiguračním parametrům (např.
SET work_mem = '...'
), máte naprostou pravdu - je to většinou to samé. Příkaz definuje nový jmenný prostor (my
) a přidáusername
variabilní do něj. Nový jmenný prostor je povinný, protože globální je vyhrazen pro konfiguraci serveru a nemůžeme do něj přidávat nové proměnné. To nám umožňuje změnit bezpečnostní politiku takto:VYTVOŘTE ZÁSADY chat_policy NA chatu POMOCÍ (current_setting('my.username') IN (message_from, message_to)) S KONTROLOU (message_from =current_setting('my.username'))Vše, co musíme udělat, je ujistit se, že fond připojení / aplikace nastaví uživatelské jméno vždy, když získá nové připojení, a přiřadí jej k uživatelské úloze.
Dovolte mi poukázat na to, že tento přístup se zhroutí, jakmile uživatelům umožníte spouštět libovolný SQL na připojení, nebo pokud se uživateli podaří objevit vhodnou zranitelnost SQL injection. V takovém případě jim nic nemůže zabránit v nastavení libovolného uživatelského jména. Ale nezoufejte, na tento problém existuje spousta řešení a my je rychle projdeme.
Proměnné podepsané relace
Prvním řešením je jednoduché vylepšení proměnných relace – skutečně nemůžeme uživatelům zabránit v nastavení libovolné hodnoty, ale co kdybychom mohli ověřit, že hodnota nebyla podvrácena? To je poměrně snadné pomocí jednoduchého digitálního podpisu. Místo pouhého uložení uživatelského jména může důvěryhodná část (fond připojení, aplikace) provést něco takového:
podpis =sha256(uživatelské jméno + časové razítko + SECRET)a poté uložte hodnotu i podpis do proměnné session:
SET my.username ='username:timestamp:signature'Za předpokladu, že uživatel nezná řetězec SECRET (např. 128B náhodných dat), nemělo by být možné změnit hodnotu bez znehodnocení podpisu.
Poznámka :Toto není nový nápad – je to v podstatě totéž jako podepsané soubory cookie HTTP. Django má o tom docela pěknou dokumentaci.
Nejjednodušší způsob, jak chránit hodnotu SECRET, je uložit ji do tabulky nepřístupné pro uživatele a poskytnout
security definer
funkce vyžadující heslo (aby uživatel nemohl jednoduše podepisovat libovolné hodnoty).CREATE FUNCTION set_username(uname TEXT, pwd TEXT) VRÁTÍ text JAKO $DECLARE v_key TEXT; v_value TEXT;BEGIN SELECT klíč_znaku INTO v_klíč FROM tajné klíče; v_value :=uname || ':' || extract(epoch from now())::int; v_hodnota :=v_hodnota || ':' || crypt(v_value || ':' || v_key, gen_salt('bf')); PERFORM set_config('my.username', v_value, false); RETURN v_value;END;$ LANGUAGE plpgsql STABILNÍ DEFINOVAČ BEZPEČNOSTI;Funkce jednoduše vyhledá podpisový klíč (tajný) v tabulce, vypočítá podpis a poté nastaví hodnotu do proměnné session. Také vrací hodnotu, většinou pro pohodlí.
Důvěryhodná část to tedy může udělat těsně před předáním připojení uživateli (samozřejmě ‚passphrase‘ není příliš dobré heslo pro produkci):
SELECT set_username('tomas', 'passphrase')A pak samozřejmě potřebujeme další funkci, která jednoduše ověří podpis a buď vypadne, nebo vrátí uživatelské jméno, pokud se podpis shoduje.
CREATE FUNCTION get_username() VRÁTÍ text JAKO $DECLARE v_key TEXT; v_části TEXT[]; v_uname TEXT; v_hodnota TEXT; v_timestamp INT; v_signature TEXT;BEGIN -- tentokrát bez ověření hesla SELECT sign_key INTO v_key FROM secrets; 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_timestamp || ':' || v_key; POKUD v_signature =crypt(v_value, v_signature) THEN RETURN v_uname; KONEC KDYŽ; RAISE EXCEPTION 'neplatné uživatelské jméno / časové razítko';END;$ LANGUAGE plpgsql STABILNÍ DEFINOVAČ BEZPEČNOSTI;A protože tato funkce nepotřebuje přístupovou frázi, může uživatel jednoduše provést toto:
SELECT get_username()Ale
get_username()
funkce je určena pro bezpečnostní politiky, např. takhle:VYTVOŘTE ZÁSADY chat_policy NA chatu POMOCÍ (get_username() IN (message_from, message_to)) S KONTROLOU (message_from =get_username())Úplnější příklad, zabalený jako jednoduché rozšíření, naleznete zde.
Všimněte si, že všechny objekty (tabulka a funkce) jsou vlastněny privilegovaným uživatelem, nikoli uživatelem přistupujícím k databázi. Uživatel má pouze
EXECUTE
oprávnění k funkcím, které jsou však definovány jakoSECURITY DEFINER
. To je důvod, proč toto schéma funguje a zároveň chrání tajemství před uživatelem. Funkce jsou definovány jakoSTABLE
, abyste omezili počet volánícrypt()
funkce (která je záměrně nákladná, aby se zabránilo hrubému násilí).Příklady funkcí rozhodně potřebují více práce. Ale doufejme, že je to dost dobré pro důkaz konceptu demonstrující, jak uložit další kontext do proměnné chráněné relace.
Ptáte se, co je potřeba opravit? Za prvé, funkce nezvládají velmi dobře různé chybové stavy. Za druhé, i když podepsaná hodnota obsahuje časové razítko, ve skutečnosti s ní nic neděláme – lze ji použít například k vypršení platnosti hodnoty. K hodnotě je možné přidat další bity, např. oddělení uživatele nebo dokonce informace o relaci (např. PID backendového procesu, aby se zabránilo opětovnému použití stejné hodnoty u jiných připojení).
Crypto
Tyto dvě funkce se spoléhají na kryptografii – kromě některých jednoduchých hashovacích funkcí toho moc nepoužíváme, ale stále jde o jednoduché kryptografické schéma. A každý ví, že byste neměli dělat vlastní kryptoměny. Proto jsem použil rozšíření pgcrypto, zejména
crypt()
funkce, abyste tento problém vyřešili. Ale nejsem kryptograf, takže i když věřím, že je celé schéma v pořádku, možná mi něco uniká – dejte mi vědět, pokud něco objevíte.Podepisování by se také skvěle hodilo pro kryptografii s veřejným klíčem – mohli bychom použít běžný klíč PGP s přístupovou frází pro podepisování a veřejnou část pro ověření podpisu. Je smutné, že ačkoli pgcrypto podporuje PGP pro šifrování, nepodporuje podepisování.
Alternativní přístupy
Samozřejmě existují různá alternativní řešení. Například místo uložení podpisového tajemství do tabulky jej můžete napevno zakódovat do funkce (ale pak se musíte ujistit, že uživatel nevidí zdrojový kód). Nebo můžete provést přihlášení pomocí funkce C, v takovém případě je skrytá před všemi, kdo nemají přístup k paměti (v takovém případě stejně přijdete).
Pokud se vám přístup k podepisování vůbec nelíbí, můžete podepsanou proměnnou nahradit tradičnějším řešením „sejfu“. Potřebujeme způsob, jak data uložit, ale musíme se ujistit, že uživatel nemůže obsah libovolně zobrazit nebo upravit, s výjimkou definovaným způsobem. Ale hej, to je to, co běžné tabulky s rozhraním API implementovaným pomocí
security definer
funkce umí!Nebudu zde uvádět celý přepracovaný příklad (úplný příklad najdete v tomto rozšíření), ale potřebujeme
sessions
stůl fungující jako trezor:CREATE TABLE sessions ( session_id UUID PRIMARY KEY, session_user NAME NOT NULL)Tabulka nesmí být přístupná běžným uživatelům databáze – jednoduché
REVOKE ALL FROM ...
by se o to měl postarat. A pak API sestávající ze dvou hlavních funkcí:
set_username(user_name, passphrase)
– vygeneruje náhodné UUID, vloží data do trezoru a uloží UUID do proměnné relaceget_username()
– načte UUID z proměnné relace a vyhledá řádek v tabulce (chyby, pokud žádný odpovídající řádek není)
Tento přístup nahrazuje ochranu podpisu náhodností UUID – uživatel může upravit proměnnou relace, ale pravděpodobnost zásahu do existujícího ID je zanedbatelná (UUID jsou 128bitové náhodné hodnoty).
Je to trochu tradičnější přístup, který se opírá o tradiční zabezpečení založené na rolích, ale má také několik nevýhod – například ve skutečnosti provádí zápisy do databáze, což znamená, že je ze své podstaty nekompatibilní se systémy v pohotovostním režimu.
Odstranění přístupové fráze
Je také možné navrhnout trezor tak, aby heslo nebylo nutné. Zavedli jsme to, protože jsme předpokládali set_username
se děje na stejném připojení – musíme ponechat funkci spustitelnou (takže zahrávání si s rolemi nebo oprávněními není řešením) a přístupová fráze zajišťuje, že ji může skutečně používat pouze důvěryhodná komponenta.
Ale co když k podepisování / vytvoření relace dojde na samostatném připojení a do připojení předaného uživateli se zkopíruje pouze výsledek (podepsaná hodnota nebo UUID relace)? Pak už přístupovou frázi nepotřebujeme. (Je to trochu podobné tomu, co dělá Kerberos – generování lístku na důvěryhodném připojení a poté použití lístku pro další služby.)
Shrnutí
Dovolte mi tedy rychle zrekapitulovat tento blogový příspěvek:
- Zatímco všechny příklady RLS používají uživatele databáze (pomocí
current_user
), není příliš obtížné zajistit, aby RLS fungovala s uživateli aplikace. - Proměnné relace jsou spolehlivým a celkem jednoduchým řešením za předpokladu, že systém má důvěryhodnou komponentu, která může nastavit proměnnou před předáním připojení uživateli.
- Když uživatel může spouštět libovolné SQL (buď záměrně, nebo díky zranitelnosti), podepsaná proměnná uživateli brání ve změně hodnoty.
- Jsou možná i jiná řešení, např. nahrazení proměnných relace tabulkou ukládající informace o relacích identifikovaných náhodným UUID.
- Pěkné je, že proměnné relace neprovádějí žádný zápis do databáze, takže tento přístup může fungovat na systémech pouze pro čtení (např. v pohotovostním režimu).
V další části této série blogů se podíváme na používání uživatelů aplikace, když systém nemá důvěryhodnou komponentu (takže nemůže nastavit proměnnou session nebo vytvořit řádek v sessions
tabulky), nebo když chceme provést (dodatečnou) vlastní autentizaci v rámci databáze.