sql >> Databáze >  >> RDS >> PostgreSQL

Uživatelé aplikace versus zabezpečení na úrovni řádků

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 do message_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á v users 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 jako SECURITY DEFINER . To je důvod, proč toto schéma funguje a zároveň chrání tajemství před uživatelem. Funkce jsou definovány jako STABLE , 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é relace
  • get_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.


  1. Existuje zkratka pro SELECT * FROM?

  2. Jak mohu vybrat všechny sloupce z tabulky plus další sloupce, jako je ROWNUM?

  3. SQL:Vytvoření relační tabulky se 2 různými auto_increment

  4. Linux – PHP 7.0 a MSSQL (Microsoft SQL)