PostgreSQL
 sql >> база данни >  >> RDS >> PostgreSQL

Потребители на приложението срещу защита на ниво ред

Преди няколко дни писах в блог за често срещаните проблеми с ролите и привилегиите, които откриваме по време на прегледи за сигурност.

Разбира се, PostgreSQL предлага много разширени функции, свързани със сигурността, една от които е защита на ниво на ред (RLS), налична от PostgreSQL 9.5.

Тъй като 9.5 беше пуснат през януари 2016 г. (така че само преди няколко месеца), RLS е сравнително нова функция и все още не се занимаваме с много производствени внедрявания. Вместо това RLS е често срещана тема на дискусиите „как да се внедри“ и един от най-често срещаните въпроси е как да го накарате да работи с потребители на ниво приложение. Така че нека видим какви възможни решения има.

Въведение в RLS

Нека първо да видим много прост пример, обясняващ за какво е RLS. Да кажем, че имаме chat таблица, съхраняваща съобщения, изпратени между потребителите – потребителите могат да вмъкват редове в нея, за да изпращат съобщения до други потребители, и да я запитват, за да видят съобщения, изпратени до тях от други потребители. Така че таблицата може да изглежда така:

CREATE TABLE chat ( message_uuid UUID ПРАВИЛЕН КЛЮЧ ПО ПОДРАЗБИРАНЕ uuid_generate_v4(), message_time TIMESTAMP NOT NULL DEFAULT now(), message_fr NAME NOT NULL DEFAULT current_user, message_to NAME NOT NULL, message_subject VARCHAR(6_text VARCHAR); /предварително> 

Класическата защита, базирана на роли, ни позволява само да ограничим достъпа до цялата таблица или до вертикални части от нея (колони). Така че не можем да го използваме, за да попречим на потребителите да четат съобщения, предназначени за други потребители, или да изпращат съобщения с фалшив message_from поле.

И точно за това служи RLS – той ви позволява да създавате правила (политики), ограничаващи достъпа до подмножества от редове. Така например можете да направите това:

СЪЗДАВАНЕ НА ПОЛИТИКА chat_policy В чат ИЗПОЛЗВАНЕ ((message_to =current_user) ИЛИ (message_from =current_user)) С ПРОВЕРКА (message_from =current_user)

Тази политика гарантира, че потребителят може да вижда само съобщения, изпратени от него или предназначени за него – това е условието в USING клаузата прави. Втората част на правилата (WITH CHECK ) гарантира, че потребителят може да вмъква съобщения само с потребителското си име в message_from колона, предотвратяваща съобщения с фалшив подател.

Можете също да си представите RLS като автоматичен начин за добавяне на допълнителни условия WHERE. Бихте могли да направите това ръчно на ниво приложение (а преди хората от RLS често правеха това), но RLS прави това по надежден и безопасен начин (например бяха положени много усилия за предотвратяване на различни течове на информация).

Забележка :Преди RLS, популярен начин да се постигне нещо подобно беше да се направи таблицата недостъпна директно (отмяна на всички привилегии) ​​и да се предостави набор от функции за дефиниране на сигурността за достъп до нея. Това постига предимно същата цел, но функциите имат различни недостатъци – те са склонни да объркват оптимизатора и сериозно да ограничават гъвкавостта (ако потребителят трябва да направи нещо и няма подходяща функция за това, той няма късмет). И разбира се, трябва да напишете тези функции.

Потребители на приложения

Ако прочетете официалната документация за RLS, може да забележите една подробност – всички примери използват current_user , т.е. текущия потребител на базата данни. Но в наши дни повечето приложения за бази данни не работят по този начин. Уеб приложенията с много регистрирани потребители не поддържат съотношение 1:1 към потребители на база данни, а вместо това използват един потребител на база данни, за да изпълняват заявки и сами да управляват потребителите на приложения – може би в users таблица.

Технически не е проблем да създадете много потребители на база данни в PostgreSQL. Базата данни трябва да се справи с това без никакви проблеми, но приложенията не го правят поради редица практически причини. Например, те трябва да проследяват допълнителна информация за всеки потребител (например отдел, позиция в организацията, данни за контакт, ...), така че приложението ще се нуждае от users маса все пак.

Друга причина може да е обединяването на връзки – използване на един споделен потребителски акаунт, въпреки че знаем, че това е разрешимо с помощта на наследяване и SET ROLE (вижте предишната публикация).

Но да приемем, че не искате да създавате отделни потребители на база данни - искате да продължите да използвате един споделен акаунт в базата данни и да използвате RLS с потребители на приложения. Как да направите това?

Променливи на сесия

По същество това, от което се нуждаем, е да предадем допълнителен контекст към сесията на базата данни, за да можем по-късно да го използваме от политиката за сигурност (вместо current_user променлива). И най-лесният начин да направите това в PostgreSQL са променливите на сесията:

ЗАДАДЕТЕ my.username ='tomas'

Ако това прилича на обичайните конфигурационни параметри (напр. SET work_mem = '...' ), напълно си прав - това е предимно едно и също нещо. Командата дефинира ново пространство от имена (my ) и добавя username променлива в него. Новото пространство за имена е задължително, тъй като глобалното е запазено за конфигурацията на сървъра и не можем да добавяме нови променливи към него. Това ни позволява да променим политиката за сигурност по следния начин:

СЪЗДАВАНЕ НА ПОЛИТИКА chat_policy В чат ИЗПОЛЗВАНЕ (current_setting('my.username') IN (message_from, message_to)) С ПРОВЕРКА (message_from =current_setting('my.username'))

Всичко, което трябва да направим, е да се уверим, че пулът за връзки/приложението задава потребителското име всеки път, когато получи нова връзка и го присвоява на потребителската задача.

Позволете ми да отбележа, че този подход се срива, след като разрешите на потребителите да изпълняват произволен SQL във връзката или ако потребителят успее да открие подходяща уязвимост за инжектиране на SQL. В този случай няма нищо, което би могло да им попречи да зададат произволно потребителско име. Но не се отчайвайте, има куп решения на този проблем и ние бързо ще ги преминем.

Подписани променливи на сесия

Първото решение е просто подобрение на променливите на сесията – не можем да попречим на потребителите да задават произволна стойност, но какво ще стане, ако можем да проверим дали стойността не е била подкопана? Това е сравнително лесно да се направи с обикновен цифров подпис. Вместо просто да съхранява потребителското име, доверената част (пул за връзки, приложение) може да направи нещо подобно:

подпис =sha256(потребителско име + времева марка + SECRET)

и след това запишете както стойността, така и подписа в променливата на сесията:

ЗАДАДЕТЕ my.username ='username:timestamp:signature'

Ако приемем, че потребителят не знае низа SECRET (напр. 128B произволни данни), не би трябвало да е възможно да се промени стойността, без да се направи невалиден подписът.

Забележка :Това не е нова идея – по същество е същото като подписаните HTTP бисквитки. Django има доста хубава документация за това.

Най-лесният начин да защитите SECRET стойността е като я съхраните в таблица, недостъпна за потребителя, и предоставите security definer функция, изискваща парола (така че потребителят да не може просто да подписва произволни стойности).

CREATE FUNCTION set_username(uname TEXT, pwd TEXT) ВРЪЩА текст КАТО $DECLARE v_key TEXT; v_value TEXT;BEGIN SELECT sign_key INTO v_key ОТ тайни; v_стойност :=име || ':' || екстракт(епоха от сега())::int; v_стойност :=v_стойност || ':' || crypt(v_value || ':' || v_key, gen_salt('bf')); PERFORM set_config('my.username', v_value, false); ВРЪЩАНЕ v_value;END;$ ЕЗИК plpgsql ДЕФИНЕР НА СИГУРНОСТ СТАБИЛЕН;

Функцията просто търси ключа за подпис (тайната) в таблица, изчислява подписа и след това задава стойността в променливата на сесията. Освен това връща стойността, най-вече за удобство.

Така че доверената част може да направи това точно преди да предаде връзката на потребителя (очевидно „парола“ не е много добра парола за производство):

ИЗБЕРЕТЕ set_username('tomas', 'passphrase')

И тогава, разбира се, имаме нужда от друга функция, която просто проверява подписа и или връща грешки, или връща потребителското име, ако подписът съвпада.

CREATE FUNCTION get_username() ВРЪЩА текст КАТО $DECLARE v_key TEXT; v_parts ТЕКСТ[]; v_uname ТЕКСТ; v_value ТЕКСТ; v_timestamp INT; v_signature TEXT;BEGIN -- този път няма проверка на паролата 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; АКО v_signature =crypt(v_value, v_signature) THEN RETURN v_uname; КРАЙ АКО; ИЗКЛЮЧВАНЕ НА ИЗКЛЮЧЕНИЕ 'невалидно потребителско име/времево клеймо';END;$ LANGUAGE plpgsql ДЕФИНЕР НА СИГУРНОСТ СТАБИЛЕН;

И тъй като тази функция не се нуждае от парола, потребителят може просто да направи това:

ИЗБЕРЕТЕ get_username()

Но get_username() функцията е предназначена за политики за сигурност, напр. така:

СЪЗДАВАНЕ НА ПОЛИТИКА chat_policy В чат ИЗПОЛЗВАНЕ (get_username() В (message_from, message_to)) С ПРОВЕРКА (message_from =get_username())

По-пълен пример, опакован като просто разширение, може да бъде намерен тук.

Забележете, че всички обекти (таблица и функции) са собственост на привилегирован потребител, а не на потребителя, който осъществява достъп до базата данни. Потребителят има само EXECUTE привилегия върху функциите, които обаче са дефинирани като SECURITY DEFINER . Това е, което кара тази схема да работи, като същевременно защитава тайната от потребителя. Функциите са дефинирани като STABLE , за да ограничите броя на извикванията към crypt() функция (която умишлено е скъпа, за да се предотврати грубата сила).

Примерните функции определено се нуждаят от повече работа. Но се надяваме, че е достатъчно добър за доказателство за концепция, демонстрираща как да съхранявате допълнителен контекст в защитена променлива на сесията.

Какво трябва да се поправи, питате? Първо, функциите не се справят много добре с различни състояния на грешки. Второ, докато подписаната стойност включва времева марка, ние всъщност не правим нищо с нея – тя може да се използва за изтичане на стойността, например. Възможно е да добавите допълнителни битове към стойността, напр. отдел на потребителя или дори информация за сесията (напр. PID на процеса на бекенда, за да се предотврати повторното използване на същата стойност при други връзки).

Крипто

Двете функции разчитат на криптография – ние не използваме много, освен някои прости функции за хеширане, но все пак това е проста крипто схема. И всички знаят, че не трябва да правите своя собствена криптовалута. Ето защо използвах разширението pgcrypto, особено crypt() функция, за да заобиколите този проблем. Но аз не съм криптограф, така че макар да вярвам, че цялата схема е наред, може би пропускам нещо – уведомете ме, ако забележите нещо.

Също така подписването би било чудесно съвпадение с криптографията с публичен ключ – бихме могли да използваме обикновен PGP ключ с парола за подписване и публичната част за проверка на подписа. За съжаление, въпреки че pgcrypto поддържа PGP за криптиране, той не поддържа подписването.

Алтернативни подходи

Разбира се, има различни алтернативни решения. Например, вместо да съхранявате тайната за подписване в таблица, можете да я кодирате твърдо във функцията (но тогава трябва да се уверите, че потребителят не може да види изходния код). Или можете да извършите подписването във функция C, като в този случай тя е скрита от всички, които нямат достъп до паметта (в този случай все пак сте загубили).

Освен това, ако изобщо не харесвате подхода на подписване, можете да замените подписаната променлива с по-традиционно решение за „трезор“. Нуждаем се от начин да съхраняваме данните, но трябва да се уверим, че потребителят не може да вижда или променя съдържанието произволно, освен по определен начин. Но хей, това е, което обикновените таблици с API са имплементирани с помощта на security definer функции могат!

Тук няма да представя целия преработен пример (проверете това разширение за пълен пример), но това, от което се нуждаем, е sessions таблица, изпълняваща ролята на трезор:

СЪЗДАВАНЕ НА ТАБЛИЦА сесии ( session_id UUID ПРАВИЛЕН КЛЮЧ, ИМЕТО НА session_user НЕ НУЛВО)

Таблицата не трябва да бъде достъпна от редовни потребители на база данни – прост REVOKE ALL FROM ... трябва да се погрижи за това. И след това API, състоящ се от две основни функции:

  • set_username(user_name, passphrase) – генерира произволен UUID, вмъква данни в трезора и съхранява UUID в променлива на сесия
  • get_username() – чете UUID от променлива на сесията и търси реда в таблицата (грешки, ако няма съвпадащ ред)

Този подход заменя защитата на подписа със случайност на UUID – потребителят може да настрои променливата на сесията, но вероятността да се удари съществуващ ID е незначителна (UUID са 128-битови произволни стойности).

Това е малко по-традиционен подход, разчитащ на традиционната сигурност, базирана на роли, но също така има няколко недостатъка – например всъщност записва база данни, което означава, че по своята същност е несъвместим със системите за гореща готовност.

Премахване на паролата

Възможно е също така да проектирате трезора, така че паролата да не е необходима. Въведохме го, защото приехме set_username се случва при същата връзка – трябва да запазим функцията изпълнима (така че бъркането с роли или привилегии не е решение), а паролата гарантира, че само довереният компонент може действително да я използва.

Но какво ще стане, ако подписването/създаването на сесия се случи на отделна връзка и само резултатът (подписана стойност или UUID на сесията) се копира в връзката, предадена на потребителя? Е, тогава вече не се нуждаем от паролата. (Това е малко подобно на това, което прави Kerberos – генериране на билет за доверена връзка, след което използване на билета за други услуги.)

Резюме

Така че позволете ми бързо да обобщя тази публикация в блога:

  • Докато всички RLS примери използват потребители на база данни (чрез current_user ), не е много трудно да накарате RLS да работи с потребители на приложения.
  • Променливите на сесията са надеждно и доста просто решение, като се приеме, че системата има доверен компонент, който може да зададе променливата, преди да предаде връзката на потребител.
  • Когато потребителят може да изпълни произволен SQL (по проект или благодарение на уязвимост), подписана променлива не позволява на потребителя да промени стойността.
  • Възможни са и други решения, напр. замяна на променливите на сесията с таблица, съхраняваща информация за сесиите, идентифицирани от произволен UUID.
  • Хубавото е, че променливите на сесията не записват в база данни, така че този подход може да работи на системи само за четене (напр. горещ режим на готовност).

В следващата част от тази серия от блогове ще разгледаме използването на потребители на приложения, когато системата няма доверен компонент (така че не може да зададе променливата на сесията или да създаде ред в sessions таблица) или когато искаме да извършим (допълнително) персонализирано удостоверяване в базата данни.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. SpringBoot+Kotlin+Postgres и JSONB:org.hibernate.MappingException:Няма диалектно съпоставяне за JDBC тип

  2. Функции на прозореца:last_value(ORDER BY ... ASC) същите като last_value(ORDER BY ... DESC)

  3. Най-добрите решения за висока достъпност за клъстериране на PG за PostgreSQL

  4. Каква е причината за грешката Повече не се разпознава... при стартиране на Postgresql 11 на машина с Windows?

  5. Как да изберете с помощта на клауза WITH RECURSIVE