[Перевод] Как работают в PostgreSQL security_barrier представления

Вы могли заметить, что в PostgreSQL 9.2 была добавлена поддержка для security_barrier представлений. Я смотрел в этот код с прицелом на добавление поддержки автоматического обновления для них, как части развивающейся работы по защите на уровне строк для проекта AXLE, и я подумал что попробую объяснить как они работают.

Роберт уже объяснил в чем польза таких представлений и от чего они защищают (кроме того, это еще обсуждалось в «Что нового в PostgreSQL 9.2»). Сейчас же я хотел бы перейти к тому, как они работают и обсудить как security_barrier представления взаимодействуют с автоматически обновляемыми представлениями.

Обычные представления


Простое обычное представление расширено в макросном виде как подзапрос, который обычно потом оптимизируется путем вынесения его предиката и добавления его в условия содержащегося запроса. Это может стать более понятно при помощи примера. Дана таблица:

CREATE TABLE t AS SELECT n, 'secret'||n AS secret FROM generate_series(1,20) n;


и представление:

CREATE VIEW t_odd AS SELECT n, secret FROM t WHERE n % 2 = 1;


запрос вида:

SELECT * FROM t_odd WHERE n < 4


будет преобразован внутри обработчика запросов в следующий вид:

SELECT * FROM (SELECT * FROM t WHERE n % 2 = 1) t_odd WHERE n < 4


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

SELECT * FROM t t_odd WHERE (n % 2 = 1) AND (n < 4)


Вы не сможете увидеть мгновенные запросы напрямую и они никогда не существую в виде реального SQL, но вы можете увидеть этот процесс, включив debug_print_parse = on, debug_print_rewritten = on и debug_print_plan = on в postgresql.conf. Я не буду воспроизводить здесь деревья разбора и планирования, так как они достаточно громоздкие и их легко сгенерировать, опираясь на приведенные выше примеры.

Проблема с использованием представлений для безопасности


Вы можете подумать что предоставление кому-то доступа к представлению, без предоставления доступа к самой таблице, не позволит им видеть четные строки. На самом деле это выглядит как правда:

regress=> SELECT * FROM t_odd WHERE n < 4;
 n | secret  
---+---------
 1 | secret1
 3 | secret3
(2 rows)


но когда вы заглянете в план, то сможете увидеть потенциальную проблему:

regress=> EXPLAIN SELECT * FROM t_odd WHERE n < 4;
                    QUERY PLAN                     
---------------------------------------------------
 Seq Scan on t  (cost=0.00..31.53 rows=2 width=36)
   Filter: ((n < 4) AND ((n % 2) = 1))
(2 rows)


Подзапрос представления был оптимизирован и его определители были вынесены во внешний запрос.

В SQL, AND и OR не упорядочены. Оптимизатор/исполнитель имеет полную свободу в выборе ветки для запуска, которую они считают более быстрой в плане выдачи ответа и возможно позволит им избежать запуска других веток. Т.е. если планировщик считает, что n < 4 намного быстрее, чем n % 2, он запустит его первым. Выглядит безвредно, верно? Попробуйте:

regress=> CREATE OR REPLACE FUNCTION f_leak(text) RETURNS boolean AS $$
BEGIN
  RAISE NOTICE 'Secret is: %',$1;
  RETURN true;
END;
$$ COST 1 LANGUAGE plpgsql;

regress=> SELECT * FROM t_odd WHERE f_leak(secret) AND n < 4;
NOTICE:  Secret is: secret1
NOTICE:  Secret is: secret2
NOTICE:  Secret is: secret3
NOTICE:  Secret is: secret4
NOTICE:  Secret is: secret5
NOTICE:  Secret is: secret6
NOTICE:  Secret is: secret7
NOTICE:  Secret is: secret8
NOTICE:  Secret is: secret9
NOTICE:  Secret is: secret10
NOTICE:  Secret is: secret11
NOTICE:  Secret is: secret12
NOTICE:  Secret is: secret13
NOTICE:  Secret is: secret14
NOTICE:  Secret is: secret15
NOTICE:  Secret is: secret16
NOTICE:  Secret is: secret17
NOTICE:  Secret is: secret18
NOTICE:  Secret is: secret19
NOTICE:  Secret is: secret20
 n | secret  
---+---------
 1 | secret1
 3 | secret3
(2 rows)

regress=> EXPLAIN SELECT * FROM t_odd WHERE f_leak(secret) AND n < 4;
                        QUERY PLAN                        
----------------------------------------------------------
 Seq Scan on t  (cost=0.00..34.60 rows=1 width=36)
   Filter: (f_leak(secret) AND (n < 4) AND ((n % 2) = 1))
(2 rows)


Упс! Как Вы можете видеть, функция с предикатом, проставляемым пользователем, считалась более дешевой для запуска чем другие тесты, поэтому она пропустила все строки, прежде чем предикат представления исключил неподходящие. Вредоносная функция может использовать тот же трюк, чтобы скопировать строки.

Представления security_barrier


Представления security_barrier исправляют это, заставляя условия представления выполняться в первую очередь, перед тем как любые условия, созданные пользователем будут применены. Вместо того, чтобы расширять представление и выносить любые условия представления во внешний запрос, они заменяют ссылку на представление подзапросом. Этот подзапрос имеет флаг security_barrier проставленным на всем диапазоне вхождения его в таблицу, который говорит оптимизатору о том, что ему не следует трогать подзапрос, или выносить условия из него, как он сделал бы в обычном случае.

Таким образом, представление с защитным барьером:

CREATE VIEW t_odd_sb WITH (security_barrier) AS SELECT n, secret FROM t WHERE n % 2 = 1;


мы получим:

regress=> SELECT * FROM t_odd_sb WHERE f_leak(secret) AND n < 4;
NOTICE:  Secret is: secret1
NOTICE:  Secret is: secret3
 n | secret  
---+---------
 1 | secret1
 3 | secret3
(2 rows)

regress=> EXPLAIN SELECT * FROM t_odd_sb WHERE f_leak(secret) AND n < 4;
                          QUERY PLAN                           
---------------------------------------------------------------
 Subquery Scan on t_odd_sb  (cost=0.00..31.55 rows=1 width=36)
   Filter: f_leak(t_odd_sb.secret)
   ->  Seq Scan on t  (cost=0.00..31.53 rows=2 width=36)
         Filter: ((n < 4) AND ((n % 2) = 1))
(4 rows)


План выполнения запроса должен сообщить Вам что происходит, хотя он и не показывает атрибута защитного барьера в выводе объяснения. Вложенный подзапрос принуждает сканировать t с условиями подзапроса представления, после чего на полученных данных выполняются условия написанной пользователем функции.

Но. Подождите секунду. Почему примененный пользователем предикат n < 4 также фигурирует в подзапросе? Разве это не потенциальная дыра в безопасности? Если n < 4 опущено, тогда почему не f_leak (secret)?

LEAKPROOF операторы и функции


Объяснение этого заключается в том, что оператор < помечен как LEAKPROOF. Этот атрибут сигнализирует о том, что данный оператор, или функция доверенно не допустит утечки информации, соответственно может быть безопасно применено к security_barrier представлениям. ПО понятным соображениям, Вы не сможете установить атрибут LEAKPROOF будучи обычным пользователем:

regress=> ALTER FUNCTION f_leak(text)  LEAKPROOF;
ERROR:  only superuser can define a leakproof function


суперпользователи же могут делать все, что захотят и им не надо прибегать к трюкам с функциями утечки информации дабы пройти защитный барьер представлений.

Почему Вы не можете обновить security_barrier представления


Обычные представления в PostgreSQL 9.3 автоматически обновляемые, но security_barrier представления не подразумевают под собой «простоты». Это связано с тем, что обновления представления полагается на возможность убрать подзапрос представления, превращая обновление в обычное обновление таблицы. Вся суть security_barrier представлений заключается в том, чтобы не позволить это исключение условия представления. UPDATE в настоящее время не может работать непосредственно с подзапросом, поэтому PostgreSQL отвергнет любые попытки обновить security_barrier представление:

regress = > UPDATE t_odd
SET
    secret = 'secret_haha' || n;
UPDATE 10 regress = > UPDATE t_odd_sb
SET
    secret = 'secret_haha' || n;
ERROR: cannot UPDATE VIEW "t_odd_sb" DETAIL: SECURITY - barrier views ARE NOT automatically updatable. HINT: TO ENABLE updating the VIEW,
provide an INSTEAD OF UPDATE TRIGGER
OR an unconditional ON UPDATE DO INSTEAD RULE.


Это именно то самое ограничение, в отмене которого я заинтересован, как части работы по развитию защиты на уровне строк для проекта AXLE. Kohei KaiGai проделал огромный объем работы с защитой на уровне строк, и такие вещи как security_barrier и LEAKPROOF в значительной степени возникли из его работы по направлению добавления защиты на уровне строк в PostgreSQL. Следующий вызов заключается в том, как иметь дело с обновлением защитного барьера безопасно и таким путем, чтобы это было обслуживаемо в будущем.

Почему подзапросы?


Вы можете задумываться, почему мы используем подзапросы для этого. Я задумывался. Короткая версия — мы не должны, но если мы не будем использовать подзапросы, нам придется вместо этого создать новые, чувствительные к сортировке вариации AND и OR операторов и научить оптимизатор, что он не может пропускать через них условия. Так как представления уже расширены подзапросами, намного проще помечать подзапросы как заборы, которые не позволяют извлекать/добавлять данные в них.

В PostgreSQL уже существует упрощенная упорядоченная операция — CASE. Проблема с использованием CASE заключается в том, никакие операции не могут пересекать границы CASE, даже LEAKPROOF. Равно как оптимизатор и не может принимать решения об использовании индексов? основываясь на выражения внутри CASE блока. Так что, если бы мы использовали CASE как я спросил об этом вот здесь, мы никогда бы нес могли использовать индекс для удовлетворения предоставленного пользователем условия.

В коде


Поддержка security_barrier была добавлена в 0e4611c0234d89e288a53351f775c59522baed7c и усилена поддержкой LEAKPROOF в cd30728fb2ed7c367d545fc14ab850b5fa2a4850. Слова благодарности идут в commit notes. Спасибо всем, кто принимал участие.

PS. Статья является относительно старой, но важна как вводная к переводу следующей статьи.

© Habrahabr.ru