44.1 pg_hba.conf: первый барьер до пароля
Прежде чем проверять пароль, сервер решает, рассматривать ли это
подключение вообще. Решает по файлу pg_hba.conf (host-based
authentication). Каждая строка отвечает: «для такого типа соединения,
такой базы, такой роли и такого адреса - проверять вот этим методом».
# TYPE DATABASE USER ADDRESS METHOD
local all all peer
host all all 10.0.0.0/8 scram-sha-256
host all all 0.0.0.0/0 trust # ОПАСНО
Строки читаются сверху вниз, побеждает первая подходящая. Метод определяет, как проверять личность:
scram-sha-256- проверка пароля по современному протоколу, боевой вариант;peer- доверие на основе имени системного пользователя, для локального сокета;trust- пускать без проверки вообще.
44.1.1 Подводный камень: trust на сетевом адресе
trust означает «пускать кого угодно без пароля». Для локального
одноразового сэндбокса это терпимо. Строка с trust на сетевом
диапазоне (0.0.0.0/0 или подсеть) - открытая дверь: достаточно
знать имя роли, и ты внутри, без всякого пароля.
Это одна из самых частых реальных дыр: trust, поставленный «на
время отладки» и забытый. Боевое правило простое - на любом сетевом
адресе метод scram-sha-256, trust только на локальном сокете и
только если очень надо.
Проверить действующие правила можно прямо из SQL, не лазая в файл:
SELECT type, database, user_name, address, auth_method
FROM pg_hba_file_rules
WHERE auth_method = 'trust';
44.2 Роли и наименьшие привилегии
В PostgreSQL нет отдельно «пользователей» и «групп» - есть роль. Роль
может логиниться (LOGIN), владеть объектами, входить в другие роли.
Привилегии выдаются через GRANT и наследуются по членству.
Базовая гигиена - приложение не ходит суперпользователем. Причина не только в «вдруг что-то сломает»: под суперпользователем не проверяются права на таблицы и не работает row-level security. Один такой коннект, утёкший наружу, открывает вообще всё, никакие GRANT и политики его не остановят.
Поэтому приложению заводят отдельную роль с правами ровно на нужные таблицы и операции - принцип наименьших привилегий. Суперпользователь остаётся для миграций и администрирования, а не для повседневных запросов приложения.
44.3 SECURITY DEFINER: исполнение с правами владельца
Обычная функция (SECURITY INVOKER, по умолчанию) исполняется с
правами того, кто её вызвал. Функция SECURITY DEFINER - с правами
её владельца. Это законный и нужный механизм: дать пользователю
выполнить контролируемое действие над данными, к которым у него нет
прямого доступа.
Пример: пользователь не имеет права писать в таблицу аудита напрямую, но может вызвать SECURITY-DEFINER-функцию, принадлежащую администратору, которая добавит туда строго одну строку нужного формата. Доступ к таблице остаётся только у функции, не у пользователя.
Проблема в том, что вместе с правами владельца функция уносит и
контекст вызвавшего - в частности, его search_path.
44.4 Эскалация через search_path
Вот коварная связка. search_path - порядок, в котором сервер ищет
схемы для неквалифицированных имён (accounts без явной public.).
Его задаёт вызывающий, и он наследуется внутрь SECURITY-DEFINER-функции.
Пусть функция принадлежит суперпользователю и содержит неквалифицированный вызов:
-- уязвимо: accounts без явной схемы
CREATE FUNCTION charge(amount numeric) RETURNS void
LANGUAGE sql SECURITY DEFINER
AS $$ UPDATE accounts SET balance = balance - amount $$;
Злоумышленник создаёт в своей схеме свою таблицу accounts, ставит
свою схему первой в search_path и вызывает charge. Функция,
исполняясь с правами суперпользователя, обратится к подделке - или,
хуже, к функции-подделке с побочным эффектом. Это полноценная
эскалация привилегий.
Защита - закрепить search_path на самой функции и квалифицировать
имена схемой:
CREATE FUNCTION charge(amount numeric) RETURNS void
LANGUAGE sql SECURITY DEFINER
SET search_path = pg_catalog, public -- фиксируем, не наследуем
AS $$ UPDATE public.accounts SET balance = balance - amount $$;
$$;
Правило без исключений: у каждой SECURITY DEFINER-функции явный
SET search_path, и имена объектов квалифицированы схемой.
44.5 Минимальный чеклист безопасности
Свести три рубежа в проверяемый список:
- в
pg_hba.confнетtrustна сетевых адресах, метод -scram-sha-256; - приложение работает не под суперпользователем, права выданы по
наименьшим привилегиям через
GRANT; - у каждой
SECURITY DEFINER-функции закреплёнsearch_pathи имена квалифицированы схемой; - row-level security включена там, где разграничение по строкам - часть модели доступа (и помни, что суперпользователь её обходит).
Этот список не заменяет полноценный аудит, но закрывает самые частые реальные дыры. Подробная справка - engine-security. А почему DROP без бэкапа - отдельная катастрофа, мы видели в главе про PITR.
Уроки в sandbox
lab-44.1. search_path и SECURITY DEFINER: воспроизведи подмену
Воспроизведём, как неквалифицированное имя в функции даёт подменить объект через search_path, и закроем дыру закреплением search_path. Полную демонстрацию с pg_hba и trust оставим топологии с рестартом; здесь - часть, которая работает на одном сервере. Сначала предскажешь, какую таблицу увидит функция, потом проверишь.
Создай «настоящую» таблицу и функцию без закреплённого search_path:
CREATE TABLE public.secret(v text); INSERT INTO public.secret VALUES ('real'); CREATE FUNCTION read_secret() RETURNS text LANGUAGE sql SECURITY DEFINER AS $$ SELECT v FROM secret LIMIT 1 $$;.Вызови функцию:
SELECT read_secret();- вернёт 'real'. Запомни.Сыграй подмену:
CREATE SCHEMA evil; CREATE TABLE evil.secret(v text); INSERT INTO evil.secret VALUES ('fake'); SET search_path = evil, public;. Предскажи, что вернёт read_secret() теперь.Проверь:
SELECT read_secret();- функция без закреплённого search_path возьмёт evil.secret и вернёт 'fake'. Имя secret разрешилось в чужую схему.Закрой дыру: пересоздай функцию защищённо -
CREATE OR REPLACE FUNCTION read_secret() RETURNS text LANGUAGE sql SECURITY DEFINER SET search_path = pg_catalog, public AS $$ SELECT v FROM public.secret LIMIT 1 $$;. СноваSELECT read_secret();при том же search_path = evil, public - теперь стабильно 'real'.
sandbox с автопроверкой - открыть в песочнице
Резюме
- pg_hba.conf - первый барьер до пароля: строки читаются сверху вниз, метод определяет, как проверять личность (scram-sha-256, peer, trust).
- trust пускает без пароля; на сетевом адресе это открытая дверь и одна из самых частых реальных дыр - на сети только scram-sha-256.
- В PostgreSQL пользователь и группа - одна сущность, роль; привилегии выдаются GRANT и наследуются по членству.
- Приложение не должно ходить суперпользователем: под ним не проверяются права на таблицы и не работает row-level security.
- SECURITY DEFINER исполняет функцию с правами владельца - законный способ дать контролируемый доступ к закрытым данным.
- Незакреплённый search_path в SECURITY DEFINER-функции - канал эскалации: подделка объекта в своей схеме выполнится с правами владельца.
- Защита - SET search_path на самой функции плюс квалификация имён схемой; это правило без исключений.
Контрольные вопросы
Почему строка с trust в pg_hba.conf на сетевом адресе так опасна?
Показать ответ
Метод trust означает, что сервер пускает подключение вообще без проверки пароля. На локальном одноразовом сокете это терпимо, но на сетевом диапазоне (например 0.0.0.0/0) это значит: кто угодно, кто знает имя роли и может дотянуться по сети, заходит в базу без всякой аутентификации. Это частая реальная дыра - trust, поставленный «на время отладки» и забытый. На сетевых адресах метод должен быть scram-sha-256.
Почему приложению нельзя ходить в базу суперпользователем?
Показать ответ
Под суперпользователем PostgreSQL не проверяет права на таблицы и не применяет row-level security - суперюзер обходит и то, и другое. Значит, ни GRANT, ни политики RLS его не ограничат. Если такой коннект (или его учётка) утечёт, открыт весь кластер. Приложению заводят отдельную роль с правами ровно на нужные таблицы и операции по принципу наименьших привилегий, а суперпользователя оставляют для миграций и администрирования.
Что делает SECURITY DEFINER и зачем он нужен?
Показать ответ
Обычная функция исполняется с правами вызвавшего (SECURITY INVOKER). SECURITY DEFINER исполняет функцию с правами её владельца. Это нужно, чтобы дать пользователю выполнить контролируемое действие над данными, к которым прямого доступа у него нет: например, добавить строго одну строку в таблицу аудита, к которой сам он писать не может. Доступ остаётся у функции, а не у пользователя.
Как незакреплённый search_path превращает SECURITY DEFINER в эскалацию привилегий?
Показать ответ
search_path задаёт вызывающий, и он наследуется внутрь SECURITY-DEFINER-функции. Если функция принадлежит суперпользователю и содержит неквалифицированное имя (accounts без схемы), злоумышленник создаёт в своей схеме объект accounts, ставит свою схему первой в search_path и вызывает функцию. Та, исполняясь с правами владельца, обращается к подделке - и выполняет её с привилегиями суперпользователя. Это и есть эскалация.
Как защитить SECURITY DEFINER-функцию от подмены через search_path?
Показать ответ
Двумя мерами вместе. Первая - закрепить search_path на самой функции через SET search_path = pg_catalog, public, чтобы она не наследовала путь вызывающего. Вторая - квалифицировать все имена объектов схемой (public.accounts, а не accounts). Тогда подсунуть объект из чужой схемы не выйдет. Это правило применяют к каждой SECURITY DEFINER-функции без исключений.