Разграничение прав доступа в базе данных

Разграничение прав доступа в базе данных

Есть разные способы показать пользователю только те данные, которые ему нужны. Разграничение прав доступа (Row level security) — один из самых универсальных, простых и надежных.

Еще по теме: Как проверить сайт на уязвимости

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

Разграничение прав доступа в базе данных

Механизм row level security позволяет реализовать разграничение доступа к данным средствами базы данных прозрачно для работающих с ней приложений. Даже если злоумышленник получил прямой доступ к базе, например под учетной записью владельца схемы с данными, RLS может не дать ему увидеть защищенную информацию. Политики RLS позволяют убирать строки из выборки целиком или скрывать значения столбцов для строк, к которым пользователь не имеет доступа. В этом отличие от обычного управления правами в БД, которые можно выдать только на объект целиком.

Как это работает? При выполнении любого запроса к базе планировщик проверяет, есть ли для этих таблиц политики доступа. Если есть, он вычисляет на основе каждой политики дополнительный предикат, который добавляет к запросу. Предикаты могут быть любой сложности. Например, вот такими:

Плюс в том, что предикаты работают для любых запросов, в том числе сделанных через инструменты администрирования (SQL Developer, Toad, PgAdmin и так далее) и даже при экспорте дампов. Это единый механизм управления доступом для всех приложений на уровне ядра СУБД. Почему он не так часто используется на практике? Вот несколько причин.

  • Людей, которые умеют работать с базами на достаточном уровне, много меньше, чем обычных программистов. Часто проще и дешевле реализовать механизмы контроля доступа в слое приложения.
  • Прозрачность. Если RLS выключен, это может обнаружиться не сразу. Приложения продолжат работать нормально, но будут выдавать данных больше, чем нужно. Само по себе это не страшно, но при плохих процессах и в сочетании с предыдущим пунктом чревато проблемами.
  • Дополнительный расход ресурсов на выполнение запроса. Обычно этот фактор не играет решающей роли. Если RLS действительно нужен, его включают и принимают чуть более медленную работу как данность. Но при неумелом применении можно тратить на RLS в разы больше, чем на полезную работу.

В общем, row level security — это инструмент для централизованного управления доступом к данным. Он реализован во многих современных СУБД — например, Oracle, PostgreSQL и MS SQL Server. В этой статье я покажу, как это работает в первых двух.

Row Level Security в Oracle

Начнем с реализации RLS в Oracle и сразу нырнем в практику.

Пример для Oracle

Мы попробуем реализовать простую политику на стандартной схеме HR. Обычный пользователь может видеть только свои данные. Руководитель департамента может видеть все данные по департаменту. Для этого нам понадобится:

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

Мы будем считать, что приложение подключается к базе под учетной записью пользователя HR и в этой программе есть инструмент аутентификации, который позволяет понять, какой именно из сотрудников с ней работает. Данные о том, какой сотрудник подключен, мы будем сохранять в контексте — специальном key-value хранилище атрибутов, управляющих приложениями. Можно использовать стандартный CLIENT_IDENTIFIER, но мы создадим свой собственный. Для этого придется создать и пакет, который будет с ним работать.

Для начала создадим от имени привилегированного пользователя контекст и сразу укажем, какой пакет может его менять:

Создаем пакет для работы с контекстом:

Теперь p_sec_context.set_employee будет использоваться приложением, чтобы задать код сотрудника в таблице EMPLOYEES, который работает в этой сессии БД.

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

При использовании CLIENT_IDENTIFIER вместо p_sec_context.set_employee нужно указать dbms_session.set_identifier.

Далее нам понадобится функция, формирующая предикат доступа. Текст, который она вернет, будет добавляться к общему блоку условий (WHERE) любого запроса, использующего таблицу через AND. Ты можешь думать об этом как о таком преобразовании: WHERE (все мои условия объединены в один блок скобками) AND предикат_доступа. Функция должна принимать два строковых параметра: имя схемы и имя таблицы. Она не должна писать ничего в базу (уровень чистоты WNDS — write no database state), но декларировать это через pragma RESTRICT_REFERENCES не требуется:

Максимальная длина получившегося условия не может превышать 32 Кбайт. В остальном можно себя не ограничивать — подзапросы, соединения таблиц и вся мощь SQL доступны.

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

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

После этого select * from EMPLOYEES начинает возвращать ноль записей. Задаем контекст и получаем полноценный доступ:

В плане запроса добавленный предикат явно виден.

Разграничение прав доступа postgresql oracle

Производительность

RLS влияет на производительность за счет двух факторов.

  1. Время на формирование дополнительного предиката. Для его сокращения нужно максимально использовать кеширование. Тип кеширования определяется типом политики. В нашем примере SHARED_CONTEXT_SENSITIVE, предикат не вычисляется повторно, если контекст сессии не менялся, и он не будет вычисляться для других объектов, использующих эту же функцию.
  2. Время на выполнение дополнительного предиката. Здесь все зависит от твоей фантазии. Он оптимизируется точно так же, как и обычные запросы.

Проблемы и возможные пути решения

  1. В этом примере при задании контекста мы доверяем вызывающему коду. Никто не мешает вызывать p_sec_context.set_employee с разными идентификаторами и проверять результаты. Можно усложнить жизнь атакующему, добавив к коду пользователя дополнительные данные, по которым проверять правильность вызова. Например, хеш от имени пользователя и текущего времени.
  2. Кое-что можно узнать в обход RLS. Oracle собирает статистику в таблицах: количество строк, наибольшее и наименьшее значение столбца, количество уникальных значений, гистограммы распределения. Статистика никак не фильтруется политиками, и эти данные видят все пользователи. Экспериментируя с внесением изменений в таблицы, где есть внешние ключи, тоже можно получить дополнительную информацию. Ограничения по уникальности также никак не учитывают политики.
  3. В этом примере код, отвечающий за безопасность, помещен в схему с данными. Это дает пользователю HR возможность просматривать его и менять. Для усложнения жизни атакующему лучше вынести объекты в отдельную схему. При желании можно и обфусцировать код, но это скорее из области замков от честных людей.
  4. Предикаты вычисляются динамически, во время выполнения, поэтому отлаживать их тяжело. С этим остается только смириться.

Row Level Security в PostgreSQL

В PostgreSQL RLS появился начиная с версии 9.5. В отличие от Oracle, политики в нем компилируемые. Это упрощает разработку и устраняет проблемы с кешами, но дает чуть меньше возможностей.

Пример для PostgreSQL

Мы будем реализовать ту же политику, которую делали на Oracle. Обычный пользователь может видеть только свои данные. Руководитель департамента может видеть все данные по нему. Если что-то пошло не так, нужно не выдавать ошибку, но и доступ к данным не разрешать. Схемы HR у нас нет, поэтому для начала создадим минимальный набор таблиц с данными:

Теперь включим RLS на таблице:

Включим RLS для ее владельца. По умолчанию они игнорируют политики доступа.

Добавим предикат доступа:

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

Настройка закончена. Смотрим план выполнения.

Можно пойти другим путем, завести для каждого пользователя сотрудника своего пользователя БД и настроить политику для учетной записи. Для начала удалим политику:

Когда операции попадают под действие нескольких политик, они объединяются операцией OR, поэтому, если бы мы не удалили test_security, запросы к employees выглядели бы как:

Создадим новую:

Эта политика позволяет пользователю увидеть только свои данные. Для этого ему придется подключаться к базе, используя учетную запись, совпадающую с его почтой, например SKING, а не общую учетку HR. В приложении ничего задавать дополнительно не требуется. Зато каждому человеку, работающему с системой, придется создать пользователя в БД со всеми правами.

Возможные проблемы

Как и в Oracle, можно частично обойти RLS, если есть доступ к статистике. Кроме этого, функции, объявленные как leakproof, могут выполняться до применения политик. Если такая функция, вопреки спецификации, раскрывает значения своих аргументов, ее можно использовать для получения информации.

Заключение

Row level security — не панацея, у него есть свои недостатки. Когда имеет смысл его применять?

  1. С данными работает больше одного приложения.
  2. Нужно реализовать разграничение доступа, не переделывая сильно программу.
  3. Нужно вынести управление доступом к данным в отдельный слой.

Когда стоит пойти другим путем?

  1. С данными гарантированно работает только одно приложение.
  2. База данных уже сильно нагружена и ограничивает производительность системы.

Так что выбирай RLS, если он тебе подходит, и, надеюсь, статья поможет тебе освоить эту технологию.

Еще по теме: Защита WHM / cPanel с помощью плагинов CSF и CXS

ВКонтакте
OK
Telegram
WhatsApp
Viber

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *