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

Vlastní poskytování uživatelských účtů v PostgreSQL prostřednictvím neprivilegovaného anonymního přístupu

Poznámka od několikanásobného:Tento blog je publikován posmrtně, protože Berend Tober zemřel 16. července 2018. Ctíme jeho příspěvky do komunity PostgreSQL a přejeme mír našemu příteli a hostujícímu spisovateli.

V předchozím článku jsme představili základy PostgreSQL triggerů a uložených funkcí a poskytli šest příkladů použití včetně ověření dat, protokolování změn, odvozování hodnot z vložených dat, skrývání dat pomocí jednoduchých aktualizovatelných pohledů, udržování souhrnných dat v samostatných tabulkách a bezpečné vyvolání kódu se zvýšeným oprávněním. Tento článek dále staví na tomto základu a představuje techniku ​​využívající spouštěč a uloženou funkci k usnadnění delegování zřizování přihlašovacích pověření na role s omezeným oprávněním (tj. bez superuživatele). Tuto funkci lze použít ke snížení administrativní zátěže pro vysoce hodnotné pracovníky správy systémů. Dovedeno do extrému, předvádíme anonymní poskytování přihlašovacích pověření koncovým uživatelem, tj. umožňujeme potenciálním uživatelům databáze, aby si sami poskytovali přihlašovací údaje implementací „dynamického SQL“ do uložené funkce prováděné na úrovni oprávnění s odpovídajícím rozsahem.Úvod

Užitečné čtení na pozadí

Nedávný článek Sebastiana Insaustiho o tom, jak zabezpečit databázi PostgreSQL obsahuje některé vysoce relevantní tipy, které byste měli znát, jmenovitě tipy č. 1 až 5 týkající se řízení autentizace klienta, konfigurace serveru, správy uživatelů a rolí, správy superuživatelů a Šifrování dat. V tomto článku použijeme části každého tipu.

Další nedávný článek od Joshuy Otwella o PostgreSQL Privileges &User Management také dobře pojednává o konfiguraci hostitele a uživatelských oprávněních, která jde o těchto dvou tématech trochu podrobněji.

Ochrana síťového provozu

Navrhovaná funkce umožňuje uživatelům poskytovat přihlašovací údaje k databázi a přitom specifikovat své nové přihlašovací jméno a heslo přes síť. Ochrana této síťové komunikace je nezbytná a lze ji dosáhnout konfigurací serveru PostgreSQL tak, aby podporoval a vyžadoval šifrovaná připojení. Zabezpečení transportní vrstvy je povoleno v souboru postgresql.conf nastavením „ssl“:

ssl = on

Řízení přístupu podle hostitele

Pro tento případ přidáme do souboru pg_hba.conf řádek konfigurace přístupu založeného na hostiteli, který umožní anonymní, tj. důvěryhodné, přihlášení do databáze z nějaké vhodné podsítě pro populaci potenciálních uživatelů databáze doslova pomocí uživatelského jména. „anonymní“ a druhý konfigurační řádek vyžadující přihlášení pomocí hesla pro jakékoli jiné přihlašovací jméno. Pamatujte, že konfigurace hostitele vyvolají první shodu, takže první řádek se použije vždy, když je zadáno „anonymní“ uživatelské jméno, což umožňuje důvěryhodné (tj. není vyžadováno heslo) připojení, a poté, kdykoli bude zadáno jakékoli jiné uživatelské jméno, bude vyžadováno heslo. Pokud má být například ukázková databáze „sampledb“ používána, řekněme, pouze zaměstnanci a interně do podnikových zařízení, pak můžeme nakonfigurovat důvěryhodný přístup pro některé nesměrovatelné interní podsítě pomocí:

# TYPE  DATABASE USER      ADDRESS        METHOD
hostssl sampledb anonymous 192.168.1.0/24 trust
hostssl sampledb all       192.168.1.0/24 md5

Pokud má být databáze obecně zpřístupněna veřejnosti, pak můžeme nakonfigurovat přístup „jakákoli adresa“:

# TYPE  DATABASE USER       ADDRESS  METHOD
hostssl sampledb anonymous  all      trust
hostssl sampledb all        all      md5

Všimněte si, že výše uvedené je potenciálně nebezpečné bez dalších opatření, možná v návrhu aplikace nebo na firewallovém zařízení, aby se rychlost omezila používání této funkce, protože víte, že některé skriptovací děťátko zautomatizuje nekonečné vytváření účtů jen pro lulz.

Všimněte si také, že jsme specifikovali typ připojení jako „hostssl“, což znamená, že připojení vytvořená pomocí TCP/IP jsou úspěšná pouze tehdy, když je připojení vytvořeno pomocí šifrování SSL, aby byl síťový provoz chráněn před odposlechem.

Uzamknutí veřejného schématu

Vzhledem k tomu, že umožňujeme přístup k databázi možná neznámým (tj. nedůvěryhodným) osobám, budeme chtít mít jistotu, že výchozí přístupy jsou omezené. Jedním z důležitých opatření je zrušit výchozí oprávnění k vytváření objektů veřejného schématu, aby se zmírnila nedávno zveřejněná zranitelnost PostgreSQL související s oprávněními výchozího schématu (srov. Skutečné zamykání veřejného schématu vaším).

Ukázková databáze

Pro ilustraci začneme prázdnou ukázkovou databází:

create database sampledb;
\connect sampledb

revoke create on schema public from public;
alter default privileges revoke all privileges on tables from public;

Vytváříme také anonymní přihlašovací roli odpovídající dřívějšímu nastavení pg_hba.conf.

create role anonymous login
    nosuperuser 
    noinherit 
    nocreatedb 
    nocreaterole 
    Noreplication;

A pak uděláme něco nového tím, že definujeme nekonvenční pohled:

create or replace view person as 
 select 
    null::name as login_name,
    null::name as login_pass;

Toto zobrazení neodkazuje na žádnou tabulku, takže výběrový dotaz vždy vrátí prázdný řádek:

select * from person;
 login_name | login_pass 
------------+-------------
            | 
(1 row)

Jedna věc, kterou to pro nás dělá, je v jistém smyslu poskytovat dokumentaci nebo nápovědu koncovým uživatelům, jaká data jsou potřebná k založení účtu. To znamená, že dotazem na tabulku, i když je výsledkem prázdný řádek, výsledek odhalí názvy dvou datových prvků.

Ale ještě lépe, existence tohoto pohledu umožňuje určit požadované datové typy:

\d person
      View "public.person"
    Column    | Type | Modifiers 
--------------+------+-----------
 login_name   | name | 
 login_pass   | name | 

Budeme implementovat funkci zřizování pověření s uloženou funkcí a spouštěčem, takže deklarujme prázdnou šablonu funkce a související spouštěč:

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();

Všimněte si, že se řídíme navrhovanou konvencí pojmenování z předchozího článku a používáme přidružený název tabulky s příponou zkrácenou zkratkou označující atributy vztahu spouštěče mezi tabulkou a uloženou funkcí pro spouštěč NAMÍSTO OF INSERT (tj. přípona „ iit”). K uložené funkci jsme také přidali atributy SCHEMA a SECURITY DEFINER:první, protože je dobrým zvykem nastavit cestu hledání, která platí po dobu provádění funkce, a druhý pro usnadnění vytváření role, což je normálně oprávnění superuživatele databáze. pouze, ale v tomto případě bude delegován na anonymní uživatele.

A nakonec přidáme minimálně dostatečná oprávnění k pohledu pro dotaz a vložení:

grant select, insert on table person to anonymous;
Stáhněte si Whitepaper Today Správa a automatizace PostgreSQL s ClusterControlZjistěte, co potřebujete vědět k nasazení, monitorování, správě a škálování PostgreSQLStáhněte si Whitepaper

Pojďme zkontrolovat

Před implementací kódu uložené funkce se podívejme, co máme. Nejprve je zde ukázková databáze vlastněná uživatelem 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 | {}

A je zde pohled, který jsme vytvořili, a seznam přístupových oprávnění pro vytváření a čtení udělených anonymnímu uživateli uživatelem 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)

Konečně, podrobnost tabulky zobrazuje názvy sloupců a datové typy a také související spouštěč:

\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()

Dynamické SQL

K vyplnění těla spouštěcí funkce použijeme dynamické SQL, tedy vytvoření konečné podoby příkazu DDL za běhu částečně z dat zadaných uživatelem. Konkrétně napevno kódujeme obrys příkazu, abychom vytvořili novou přihlašovací roli a vyplnili specifické parametry jako proměnné.

Obecná forma tohoto příkazu je

create role name [ [ with ] option [ ... ] ]

kde možnost může být kterákoli ze šestnácti specifických vlastností. Obecně platí, že výchozí hodnoty jsou vhodné, ale vysvětlíme si několik možností omezení a použijeme formulář

create role name 
  with 
    login 
    inherit 
    nosuperuser 
    nocreatedb 
    nocreaterole 
    password ‘password’;

kam za běhu vložíme uživatelem zadaný název role a heslo.

Dynamicky konstruované příkazy se vyvolávají příkazem execute:

execute command-string [ INTO [STRICT] target ] [ USING expression [, ... ] ];

která by pro naše konkrétní potřeby vypadala

  execute 'create role '
    || new.login_name
    || ' with login inherit nosuperuser nocreatedb nocreaterole password '
    || quote_literal(new.login_pass);

kde funkce quote_literal vrací argument řetězce vhodně citovaný pro použití jako řetězcový literál tak, aby byl splněn syntaktický požadavek, aby bylo heslo skutečně uvozováno..

Jakmile máme vytvořený příkazový řetězec, dodáme jej jako argument příkazu pl/pgsql v rámci spouštěcí funkce.

Když to dáme dohromady, vypadá to takto:

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;
  $$;

Vyzkoušíme to!

Vše je na svém místě, tak se na to pojďme podívat! Nejprve přepneme autorizaci relace na anonymního uživatele a poté provedeme vložení do pohledu osoby:

set session authorization anonymous;
insert into person values ('alice', '1234');

Výsledkem je, že do systémové tabulky byl přidán nový uživatel Alice:

\du
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 alice     |                                                            | {}
 anonymous | No inheritance                                             | {}
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

Funguje dokonce přímo z příkazového řádku operačního systému tím, že do klientského nástroje psql přidá řetězec příkazů SQL pomocí kanálu pro přidání uživatele 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 | {}

Použijte nějaké brnění

Počáteční příklad spouštěcí funkce je zranitelný vůči útoku SQL injection, tj. nebezpečný aktér hrozby by mohl vytvořit vstup, který by vedl k neoprávněnému přístupu. Pokud jste například připojeni jako role anonymního uživatele, pokus o provedení něčeho mimo rozsah selže správně:

set session authorization anonymous;
drop user alice;
ERROR:  permission denied to drop role

Ale následující škodlivý vstup vytvoří roli superuživatele s názvem „eve“ (a také návnadu s názvem „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 | {}

Roli skrytého superuživatele pak lze použít k narušení databáze, například smazáním uživatelských účtů (nebo ještě hůř!):

\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 | {}

Abychom tuto zranitelnost zmírnili, musíme podniknout kroky k dezinfekci vstupu. Například použití funkce quote_ident, která vrací řetězec vhodně citovaný pro použití jako identifikátor v příkazu SQL s uvozovkami přidanými v případě potřeby, například pokud řetězec obsahuje znaky bez identifikátoru nebo by se skládala velká a malá písmena, a správně zdvojené vložené uvozovky:

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;
  $$;

Nyní, pokud se stejný exploit SQL injection pokusí vytvořit dalšího superuživatele jménem „frank“, selže a výsledkem je velmi neortodoxní uživatelské jméno:

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 | {}

Můžeme použít další rozumné ověření dat v rámci spouštěcí funkce, jako je vyžadování pouze alfanumerických uživatelských jmen a odmítnutí mezer a dalších znaků:

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 poté potvrďte, že různé dezinfekční kontroly fungují:

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.

Pojďme to vylepšit

Předpokládejme, že chceme uložit další metadata nebo data aplikace související s vytvořenou uživatelskou rolí, například časové razítko a zdrojovou IP adresu spojenou s vytvořením role. Pohled samozřejmě nemůže splnit tento nový požadavek, protože neexistuje žádné základní úložiště, takže je vyžadována skutečná tabulka. Předpokládejme také, že chceme omezit viditelnost této tabulky pro uživatele, kteří se přihlašují pomocí role anonymního přihlášení. Tabulku můžeme skrýt do samostatného jmenného prostoru (tj. schéma PostgreSQL), který zůstane anonymním uživatelům nepřístupný. Nazvěme tento jmenný prostor „soukromý“ jmenný prostor a vytvořte tabulku v jmenném prostoru:

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()  
);

Jednoduchý dodatečný příkaz vložení uvnitř spouštěcí funkce zaznamená tato související metadata:

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;
  $$;

A můžeme to dát jednoduchým testem. Nejprve potvrzujeme, že při připojení jako anonymní role je viditelný pouze pohled public.person a nikoli tabulka 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 poté po vložení nové role:

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)

tabulka private.person ukazuje zachycení metadat pro IP adresu a čas vložení řádku.

Závěr

V tomto článku jsme demonstrovali techniku ​​delegování poskytování pověření role PostgreSQL na role bez superuživatele. Zatímco příklad plně delegoval funkcionalitu pověření anonymním uživatelům, podobný přístup by bylo možné použít k částečnému delegování funkcí pouze důvěryhodným pracovníkům a zároveň si zachovat výhodu přenesení této práce od vysoce hodnotných pracovníků správce databází nebo systémů. Demonstrovali jsme také techniku ​​vrstveného přístupu k datům využívající schémata PostgreSQL, selektivní odhalování nebo skrývání databázových objektů. V dalším článku této série se budeme zabývat technikou vrstveného přístupu k datům a navrhneme nový návrh architektury databáze pro implementace aplikací.


  1. Najděte maximální počet po sobě jdoucích let pro každé ID v tabulce (Oracle SQL)

  2. Jaký je rozdíl mezi jednoduchými a dvojitými uvozovkami v PostgreSQL?

  3. Pandy zapisují datový rámec do jiného schématu postgresql

  4. PostgreSQL je pomalý na velké tabulce s poli a spoustou aktualizací