Zabezpečení databáze a dat
Základní doporučení z hlediska bezpečnosti přístupu
- POZOR NA SQL INJECTION!!! Primárně se jedná o aplikační problém, který nelze vyřešit na db úrovni. Na úrovni databáze lze maximálně minimalizovat dopad průniku.
- Používejte více databázových (případně systémových) účtů.
- Každá role musí mít přístup pouze k těm datům, které potřebuje pro svou činnost.
- Aplikační databázový uživatel nikdy nesmí být vlastníkem databázových objektů (vlastníkem tabulek, funkcí, databází).
- Je praktické používat pouze jeden vyhrazený účet, který slouží jako vlastník všech db objektů - tento účet (pokud možno) nikdy nepoužíváme z veřejně dostupné aplikace! Např. omezíme možnost přihlášení pouze z lokální sítě. Důležité: heslo k tomuto účtu není nikde uloženo (maximálně někde v zapečetěné obálce v trezoru).
- Přístup nastavujte explicitně - příkazem GRANT, nikoliv implicitně (skrz vlastníka) - donutí to přemýšlet o zabezpečení.
- Pokud používáte SECURITY DEFINER funkce, tak explicitně nastavujte SEARCH_PATH jako atribut funkce.
Kontrola vynucení zadání hesla (pg_hba.conf)
PostgreSQL umožňuje nezabezpečený přístup k databázi (který je ve výchozí konfiguraci) vždy neaktivní. Při kontrole zabezpečení je vhodné si zkontrolovat obsah konfiguračního souboru 'pg_hba.conf'. Tento soubor by nesmí obsahovat nezakomentované řádky, kde je metoda ověřování "trust".
SQL injection
SQL injection je nejběžnější útok na aplikace používající SQL databáze. Podobně jako při jiných typech útoků, kdy aplikace vykoná podstrčený (infikovaný) kód, zde aplikace vykoná podstrčený SQL příkaz (nebo jeho fragment, tj dojde k modifikaci zabudovaného SQL příkazu). Pokud vývojář správně neošetří vstup a použije dnes již nedoporučované techniky parametrizace SQL příkazů, jako je sčítání neošetřených vstupů, pak útočník může změnit charakter SQL příkazu. Zabezpečení proti SQL injection je jednoduché a spolehlivé - stačí nepoužívat nebezpečné techniky. Je důležité si uvědomit, že se jedná o aplikační problém, a že se nemusí jednat pouze o www aplikaci (většina aplikací, které jsme psali před rokem 2000 ve Visual Basicu byla děravá jako řešeto).
# ověření hesla # CHYBA NEPOUZIVAT UKAZKA SQL INJECTION !!! $heslo = ... # hodnota ze vstupu -- utocnik pouzije heslo " ' or 1=1 -- " $uzivatel = 'xxx'; # sestaveni dotazu -- POZOR SQL INJECTION !!! $sql = "SELECT * FROM users WHERE user='$uzivatel' AND password='$heslo'; # vysledkem je dotaz SELECT * FROM users WHERE user='xxx' AND password=' ' or 1=1 -- ' # tento prikaz je syntakticky spravny a vzdy vraci jeden nebo vice radku, bez ohledu # na zadane vstupy
Pozor, je nutné zabezpečit všechny SQL operace (NEJEN LOGIN). Tady jako běžný uživatel zadávám nový tiket a při následném výpisu tiketů uvidím administrátorské heslo:
postgres=# create table users(name text, password text); CREATE TABLE postgres=# create table tickets(inserted timestamp default current_timestamp, other text); CREATE TABLE postgres=# insert into users values('admin','mysecretpass'); INSERT 0 1 -- útok prostřednictvím obyčejného uživatele -- Jako obsah pole "other" bych zvolil řetězec: "' || (select password from users where name = 'admin') || '" postgres=# insert into tickets(other) values((select password from users where name = 'admin')); INSERT 0 1 postgres=# select * from tickets; inserted | other ----------------------------+-------------- 2015-02-14 11:18:41.434376 | mysecretpass (1 row)
Ochrana
Každé vývojové prostředí nabízí bezpečné API pro imunizaci vstupů (escaping) nebo pro předávání parametrů dotazu odděleně - a je třeba o nich vědět a používat je. Může to být použití prepared statements (předpřipravených dotazů), může to být použití implicitního escapeování, případně explicitního. Vždy je však nutné v hostitelském prostředí (PHP, Perl, ...) nějakým jasně definovaným způsobem oddělit SQL příkaz a jeho parametry.
Nikdy nepoužívat skládání řetězců!!!
Pozor! Je chybou se soustředit pouze na zabezpečení LOGINu. U většiny systémů si útočníci mohou vytvořit účet legálně - a pak tento účet použít pro útok.
Logujte chyby - pokus o průnik v chybně zabezpečené aplikaci se většinou projeví množstvím syntaktických chyb. Pozor! V ten okamžik víte, že máte děravou aplikaci. Ale v tu chvíli útočník může stahovat citlivá data.
Ukázka bezpečného kódu s využitím rozhraní PDO v PHP:
<?php $dbh = new PDO("pgsql:dbname=postgres;host=localhost", "pavel", "" ); $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $usename = 'pavel'; /*** prepare the SQL statement ***/ $stmt = $dbh->prepare("SELECT * FROM pg_user WHERE usename = :usename"); /*** bind the paramaters ***/ $stmt->bindParam(':usename', $usename, PDO::PARAM_STR, 64); /*** execute the prepared statement ***/ $stmt->execute(); /*** fetch the results ***/ $result = $stmt->fetchAll(); /*** loop of the results ***/ foreach($result as $row) { echo $row['usename'].'<br />'."\n"; echo $row['usesysid'].'<br />'."\n"; } /*** close the database connection ***/ $dbh = null; ?>
Kontrola
Pro databázový účet, který používá aplikace zapneme logování všech dotazů:
postgres=# ALTER ROLE pavel SET log_min_duration_statement = 0; ALTER ROLE
V logu Postgresu pak bychom měli vidět záznam o použití prepared statement:
LOG: duration: 0.681 ms parse pdo_stmt_00000001: SELECT * FROM pg_user WHERE usename = $1 LOG: duration: 0.757 ms bind pdo_stmt_00000001: SELECT * FROM pg_user WHERE usename = $1 DETAIL: parameters: $1 = 'pavel' LOG: duration: 0.103 ms execute pdo_stmt_00000001: SELECT * FROM pg_user WHERE usename = $1 DETAIL: parameters: $1 = 'pavel' LOG: duration: 0.114 ms statement: DEALLOCATE pdo_stmt_00000001
Poznámka - Starší verze PostgreSQL mohly mít určité problémy s efektivitou optimalizace prepared statements z důvodu optimalizace naslepo. Tento problém by měl být vyřešen ve verzi 9.2 a novějších.
Verifikace hesla bez možnosti čtení tabulky s uloženými hesly
Aplikační databázový uživatel musí mít přístup k tabulce obsahující seznam uživatelů a hashi hesel, aby mohl ověřit účet a heslo. To ale znamená významné bezpečnostní riziko, pokud by došlo ke kompromitování aplikačního databázového účtu. Pak je možné si stáhnout seznam uživatelů. Možným řešením je použití SECURITY DEFINER funkce - to je funkce, jejíž tělo je výkonáváno s právy vlastníka funkce (nikoliv s právy volajícího). Tato funkce umožní vykonání jasně definovaného bezpečného kódu s dočasně změněnou identitou. V případě kompromitace účtu může účastník volat tyto funkce, ale nemá přístup k tabulce uživatelů/hesel.
postgres=# CREATE ROLE eshop_owner NOLOGIN; CREATE ROLE postgres=# CREATE ROLE eshop_app LOGIN; CREATE ROLE postgres=# ALTER ROLE eshop_app PASSWORD 'bezpecnejsiHeslo'; ALTER ROLE SET ROLE eshop_owner; CREATE TABLE eshop_users(account text CHECK (account <> '') PRIMARY KEY, password text CHECK (password <> '')); CREATE OR REPLACE FUNCTION public.new_account(account text, password text) RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO pg_catalog AS $function$ BEGIN IF coalesce(trim(new_account.account), '') = '' THEN RAISE EXCEPTION 'cannot create account' USING DETAIL='Name is empty string.'; ELSEIF coalesce(trim(new_account.password), '') = '' THEN RAISE EXCEPTION 'cannot create account "%"', new_account.account USING DETAIL='Password is empty string'; END IF; INSERT INTO public.eshop_users(account,password) VALUES(trim(new_account.account), md5(new_account.password || trim(new_account.account) || 'somerandomstr')); EXCEPTION WHEN unique_violation THEN PERFORM pg_sleep(random()); RAISE EXCEPTION 'cannot create account "%"', new_account.account USING DETAIL='User already exists'; WHEN others THEN PERFORM pg_sleep(random()); -- reraise exception RAISE; END; $function$; CREATE OR REPLACE FUNCTION public.verify_password(account text, password text) RETURNS boolean LANGUAGE plpgsql SECURITY DEFINER SET search_path TO pg_catalog AS $function$ BEGIN IF EXISTS(SELECT * FROM public.eshop_users e WHERE e.account = trim(verify_password.account) AND e.password=md5(verify_password.password || trim(verify_password.account) || 'somerandomstr')) THEN RETURN true; ELSE PERFORM pg_sleep(random()); RETURN false; END IF; END; $function$; postgres=> SET ROLE eshop_app; SET postgres=> SELECT * FROM eshop_users; ERROR: permission denied for relation eshop_users postgres=> SELECT new_account('pavel','mojeheslo'); new_account ------------- (1 row) postgres=> SELECT new_account('pavel','mojeheslo'); ERROR: cannot create account "pavel" DETAIL: User already exists postgres=> SELECT verify_password('pavel','mojeheslo'); verify_password ----------------- t (1 row) postgres=> SELECT verify_password('pavel','mojehesloa'); verify_password ----------------- f (1 row)
Výše uvedený kód ochrání přístup k citlivým údajům v případě že dojde ke kompromitaci aplikačního uživatele (což je nejčastější varianta - např. použitím SQL injection).
V případě kompromitace účtu s právy administrátora může dojít k několika možným rizikům:
- přístup k logům Postgresu, kde mohou být zalogované SQL příkazy a jejich parametry (tudíž i hesla). Toto riziko lze odstranit vytvořením hashe na už na straně klienta - nicméně získání hashe znamená také výrazné bezpečnostní riziko.
- plný přístup do databáze - administrátor se může přihlásit jako uživatel s právy superusera.
- možnost odposlechu síťového provozu - toto riziko hrozí v případě, že databáze je na jiném serveru než je aplikační server. Tomuto riziku lze zabránit pouze využitím šifrované komunikace - např. ssl, ssh tunel, ...
Proto, vždy se přihlašujte k silnějším účtům pomocí bezpečného (šifrovaného) kanálu z bezpečné sítě! Svůj operační systém udržujte aktuální - tak aby v případě kompromitování aplikačního účtu bylo maximálně sníženo riziko získání neoprávněného získání práv administrátora.
Na co si dát pozor
Komunikace po síti
Ve výchozím nastavení (některé distribuce jsou vyjímkou - např. Debian) PostgreSQL nešifruje komunikaci mezi klientem a serverem. Data jsou posílaná v textové čitelné podobě a hrozí riziko zachycení a monitorování komunikace. Jedinou vyjímkou je posílání hesel, kdy se posílá pouze osolený hash - tj. přihlášení k databázi je bezpečné - zbytek už ale ne. Jednoduše lze zapnout podporu ssl, které zajistí šifrování komunikace a odolnost vůči odposlechu. Pozor - ssl protokol může výrazně zpomalit přihlašování do databáze - pokud máte aplikaci, která často navazuje spojení, tak je pak vhodné uvažovat o poolování spojení.
Nastavení SSL
Jednoduchý postup pro nastavení šifrované komunikace bez ověřování certifikátu serveru:
- Generování certifikátu
openssl req -new -text -out cert.req openssl rsa -in privkey.pem -out cert.pe openssl req -x509 -in cert.req -text -key cert.pem -out cert.cert cp cert.pem $PGDATA/server.key cp cert.cert $PGDATA/server.crt chmod 600 $PGDATA/server.key chmod 600 $PGDATA/server.crt
- V konfiguračním souboru postgresu (postgresql.conf) zapnout podporu ssl
ssl=on
- V souboru pg_hba.conf si vynutit použití ssl, nezapomenout zkontrolovat (a nastavit pořadí) ostatních host* konfigurací
hostssl all all 0.0.0.0/0 md5
- Restart serveru
Kontrola přihlášení:
[pavel@localhost ~]$ /usr/local/pgsql/bin/psql postgres -h 127.0.0.1 Password: psql (9.4devel) SSL connection (cipher: DHE-RSA-AES256-SHA, bits: 256) Type "help" for help. postgres=#
Bezpečné techniky zabezpečení přístupu k datům pomocí pohledů
Jedním z možných využití pohledů (Views) je omezení přístupu k datům. Prostřednictvím pohledů přistupujeme k tabulkám s právy vlastníka pohledu a stejně tak, jako tabulkám, nastavujeme přístupová práva k pohledu.
postgres=# SET ROLE eshop_owner; SET postgres=> CREATE TABLE zamestnanci(jmeno text, mzda integer); CREATE TABLE postgres=> DROP TABLE zamestnanci ; DROP TABLE postgres=> CREATE TABLE zamestnanci(jmeno text NOT NULL, mzda integer NOT NULL); CREATE TABLE postgres=> INSERT INTO zamestnanci VALUES('Tomas', 10000); INSERT 0 1 postgres=> CREATE VIEW zamestnanci_public AS SELECT jmeno FROM zamestnanci; CREATE VIEW postgres=> GRANT SELECT ON zamestnanci_public TO public; GRANT postgres=> SET ROLE eshop_app; SET postgres=> SELECT * FROM zamestnanci; ERROR: permission denied for relation zamestnanci postgres=> SELECT * FROM zamestnanci_public; jmeno ------- Tomas (1 row)
Podobně můžeme pohledy použít pro omezení přístupu k některým řádkům:
postgres=> SET ROLE eshop_owner; SET postgres=> CREATE TABLE zamestnanci(jmeno text NOT NULL, mzda integer NOT NULL, kategorie int); CREATE TABLE postgres=> INSERT INTO zamestnanci VALUES('Tomas', 10000, 1); INSERT 0 1 postgres=> INSERT INTO zamestnanci VALUES('Jirka', 20000, 2); INSERT 0 1 postgres=> CREATE VIEW zamestnanci2_public AS SELECT jmeno, mzda, kategorie FROM zamestnanci WHERE kategorie = 1; CREATE VIEW postgres=> GRANT SELECT ON zamestnanci2_public TO public; GRANT postgres=> SET ROLE eshop_app; SET postgres=> SELECT * FROM zamestnanci; ERROR: permission denied for relation zamestnanci postgres=> SELECT * FROM zamestnanci2_public ; jmeno | mzda | kategorie -------+-------+----------- Tomas | 10000 | 1 (1 row) postgres=> SELECT * FROM zamestnanci2_public WHERE kategorie = 2; jmeno | mzda | kategorie -------+------+----------- (0 rows)
Na první pohled vypadá, že je vše ok. Ale to bohužel není pravda. Pokud některý uživatel má možnost si vytvářet funkce v Postgresu a používat ladící výstup, tak si může napsat funkci, kterou optimalizátor předsune před filtr kategorie (z důvodu nižší ceny).
postgres=> CREATE OR REPLACE FUNCTION show(text, int, int) RETURNS boolean AS $$ BEGIN RAISE NOTICE '% % %', $1, $2, $3; RETURN true; END; $$ LANGUAGE plpgsql COST 0.000000001; CREATE FUNCTION postgres=> EXPLAIN VERBOSE SELECT * FROM zamestnanci2_public WHERE show(jmeno, mzda, kategorie); QUERY PLAN -------------------------------------------------------------------------------------------------------------- Seq Scan on public.zamestnanci (cost=0.00..24.50 rows=2 width=40) Output: zamestnanci.jmeno, zamestnanci.mzda, zamestnanci.kategorie Filter: (show(zamestnanci.jmeno, zamestnanci.mzda, zamestnanci.kategorie) AND (zamestnanci.kategorie = 1)) (3 rows) postgres=> SELECT * FROM zamestnanci2_public WHERE show(jmeno, mzda, kategorie); NOTICE: Tomas 10000 1 NOTICE: Jirka 20000 2 jmeno | mzda | kategorie -------+-------+----------- Tomas | 10000 | 1 (1 row)
Pohledy se většinou nepoužívají pro omezení přístupu - proto není snaha omezit optimalizátor v PostgreSQL, aby respektoval pořadí zadání predikátů. Naopak, většinou chceme, aby PostgreSQL zvolil zvolil plán, jehož cena je nejnižší a je pravděpodobné, že jeho spočítání bude nejrychlejší. Nicméně problém zůstává - a řeší se pomocí atributu "security_barrier". Pokud je pohled označkován tímto atributem, tak pak optimalizátor ví, že je pohled použitý jako zabezpečení přístupu k datům, a optimalizátor vždy provede nejdříve predikáty uvedené v definici pohledu:
postgres=> SET ROLE eshop_owner; SET postgres=> CREATE OR REPLACE VIEW zamestnanci2_public WITH (security_barrier) AS SELECT jmeno, mzda, kategorie FROM zamestnanci WHERE kategorie = 1; CREATE VIEW postgres=> SET ROLE eshop_app; SET postgres=> EXPLAIN SELECT * FROM zamestnanci2_public WHERE show(jmeno, mzda, kategorie); QUERY PLAN ---------------------------------------------------------------------------------------------------- Subquery Scan on zamestnanci2_public (cost=0.00..24.56 rows=2 width=40) Filter: show(zamestnanci2_public.jmeno, zamestnanci2_public.mzda, zamestnanci2_public.kategorie) -> Seq Scan on zamestnanci (cost=0.00..24.50 rows=6 width=40) Filter: (kategorie = 1) (4 rows) postgres=> SELECT * FROM zamestnanci2_public WHERE show(jmeno, mzda, kategorie); NOTICE: Tomas 10000 1 jmeno | mzda | kategorie -------+-------+----------- Tomas | 10000 | 1 (1 row)