Интерфейсы передачи указателей
Содержание
1. Обзор
Три новых интерфейса "_pointer()" были добавлены в SQLite 3.20.0 (2017-08-01):
На почтовых списках появились вопросы и путаница относительно целей этих новых интерфейсов, причин их внедрения и решаемых ими проблем. Эта статья пытается ответить на эти вопросы и прояснить ситуацию.
2. Краткая история передачи указателей в SQLite
Иногда расширения SQLite удобно обмениваются значениями, отличными от SQL, между подкомпонентами или между расширением и приложением. Вот несколько примеров:
В расширении FTS3, оператор MATCH (выполняющий полнотекстовый поиск) должен передавать информацию о совпавших записях функциям snippet(), offsets() и matchinfo(), чтобы они могли преобразовать эти данные в полезный результат.
Для того, чтобы приложение могло добавлять новые расширения к FTS5, такие как новые токенизаторы, ему нужен указатель на объект "fts5_api".
В расширении CARRAY приложение должно указать расширению расположение массива данных C-языка, содержащего данные для табличной функции, которую реализует расширение.
Традиционным способом передачи этой информации было преобразование C-указателя в BLOB или 64-битное целое число, затем передача этого BLOB или целого числа через SQLite с использованием стандартных интерфейсов, таких как sqlite3_bind_blob(), sqlite3_result_blob(), sqlite3_value_blob() или их целые аналоги.
2.1. Повышение уровня угрозы
Передача указателей как целых чисел или BLOB — это просто, эффективно и хорошо работает в среде, где все компоненты приложения дружественны друг к другу. Однако передача указателей как целых чисел и BLOB позволяет злонамеренному SQL-коду создавать недействительные указатели, которые могут причинять вред.
Например, первый аргумент функции snippet() должен быть специальным столбцом таблицы FTS3, содержащим указатель на объект fts3cursor, который содержит информацию о текущем совпадении полнотекстового поиска. Раньше этот указатель передавался как BLOB. Например, если таблица FTS3 называется "t1", а столбец — "cx", можно было написать:
SELECT snippet(t1) FROM t1 WHERE cx MATCH $pattern;
Но если хакер может выполнить произвольный SQL-запрос, он может выполнить немного другой запрос, например:
SELECT hex(t1) FROM t1 WHERE cx MATCH $pattern;
Поскольку указатель передается в столбце t1 таблицы t1 как BLOB (в более ранних версиях SQLite), такой запрос отобразил бы значение указателя в шестнадцатеричном формате. Затем злоумышленник мог бы изменить этот указатель, чтобы попытаться заставить функцию snippet() изменить память в какой-либо другой части адресного пространства приложения вместо объекта fts3cursor, на котором она должна была работать:
SELECT snippet(x'6092310100000000') FROM t1 WHERE cx MATCH $pattern;
Исторически это не считалось угрозой. Аргументом было то, что если злоумышленник способен ввести произвольный SQL-текст в приложение, то он уже полностью контролирует приложение, поэтому создание поддельного указателя не даёт злоумышленнику новых возможностей.
В большинстве случаев потенциальные злоумышленники не имеют возможности внедрить произвольный SQL-код, и большинство применений SQLite защищены от атаки выше. Но есть и заметные исключения.
Интерфейс WebSQL для webkit позволял любой веб-странице выполнять произвольный SQL-код в браузере Chrome и Safari. Этот произвольный SQL-код должен был выполняться в песочнице, где он не мог причинить вреда, даже если его эксплуатировали, но эта песочница оказалась менее безопасной, чем предполагалось. Весной 2017 года одна команда хакеров смогла взломать iMac, используя цепочку эксплойтов, одним из которых было повреждение указателей, передаваемых как значения BLOB функции snippet() FTS3 базы данных SQLite, работавшей через интерфейс WebSQL в Safari.
На Android, как сообщается, существует множество сервисов, которые бездумно выполняют произвольный SQL-код, передаваемый им ненадежными приложениями, загруженными из сомнительных источников в интернете. Сервисы Android, как предполагается, более тщательно относятся к выполнению SQL-кода из ненадежных источников. Автор не имеет конкретных примеров обратного, но слышал слухи о таких случаях. Даже если все сервисы Android более осторожны и правильно проверяют весь запускаемый ими SQL-код, было бы сложно проверить их все, чтобы убедиться в их безопасности. Поэтому люди, заботящиеся о безопасности, стремятся предотвратить возможность эксплуатации.
Система управления версиями Fossil (разработанная и написанная для поддержки разработки SQLite) позволяет пользователям с ограниченным доверием вводить произвольный SQL для создания отчетов о проблемах. Этот SQL очищается с помощью интерфейса sqlite3_set_authorizer(), и никаких эксплойтов никогда не находили. Но это пример потенциально враждебных агентов, способных внедрить произвольный SQL-код в систему.
2.2. Предотвращение подделки указателей
Первая попытка устранения брешей в безопасности при передаче указателей заключалась в предотвращении создания поддельных указателей. Это было достигнуто путем добавления подтипа к каждому указателю с помощью sqlite3_result_subtype() и проверкой подтипа получателем с помощью sqlite3_value_subtype(), отбрасывая указатели с неправильным подтипом. Поскольку нет способа добавить подтип к результату с помощью чистого SQL, это предотвращает подделку указателей с помощью SQL. Единственный способ передачи указателя — это код C. Если злоумышленник может установить подтип, то он также может подделать указатель без помощи SQLite.
Использование подтипов для идентификации допустимых указателей предотвратило эксплуатацию WebSQL. Но оказалось, что это неполное решение.
2.3. Утечки указателей
Использование подтипов для указателей предотвращало подделку указателей с помощью чистого SQL. Но подтипы ничего не делают, чтобы предотвратить чтение значений указателей. Другими словами, подтипы значений указателей предотвращают атаки с помощью SQL-запросов, подобных этому:
SELECT snippet(x'6092310100000000') FROM t1 WHERE cx MATCH $pattern;
Аргумент BLOB для snippet() не имеет правильного подтипа, поэтому функция snippet игнорирует его, не вносит изменений в структуры данных и безопасно возвращает NULL.
Но использование подтипов не делает ничего, чтобы предотвратить чтение значения указателя с помощью SQL-кода, подобного этому:
SELECT hex(t1) FROM t1 WHERE cx MATCH $pattern;
Какой вред может быть от этого, спросите вы? Разработчики SQLite (в том числе и автор) задавались тем же вопросом. Но затем исследователи в области безопасности указали, что знание указателей может помочь злоумышленникам обойти защиты случайного размещения адресного пространства. Это называется "утечкой указателей". Утечка указателя сама по себе не является уязвимостью, но она может помочь злоумышленнику эффективно использовать другие уязвимости.
3. Новые интерфейсы передачи указателей
Для обеспечения безопасной передачи частной информации между компонентами расширений без утечек указателей требуются новые интерфейсы:
- sqlite3_bind_pointer(S,I,P,T,D) → Связывает указатель P типа T с I-м параметром подготовленного запроса S. D — это необязательная функция-деструктор для P.
- sqlite3_result_pointer(C,P,T,D) → Возвращает указатель P типа T в качестве аргумента функции C. D — это необязательная функция-деструктор для P.
- sqlite3_value_pointer(V,T) → Возвращает указатель типа T, связанный со значением V, или NULL, если у V нет связанного указателя или если указатель V имеет тип, отличный от T.
Для SQL значения, создаваемые с помощью sqlite3_bind_pointer() и sqlite3_result_pointer(), неотличимы от NULL. SQL-запрос, который пытается использовать функцию hex() для чтения значения указателя, получит SQL-NULL в качестве ответа. Единственный способ определить, есть ли связанный указатель, — это использовать интерфейс sqlite3_value_pointer() с соответствующей строкой типа T.
Значения указателей, считанные с помощью sqlite3_value_pointer(), не могут быть сгенерированы чистым SQL-кодом. Поэтому подделка указателей с помощью SQL невозможна.
Значения указателей, создаваемые с помощью sqlite3_bind_pointer() и sqlite3_result_pointer(), не могут быть прочитаны чистым SQL-кодом. Поэтому утечка значений указателей с помощью SQL невозможна.
Таким образом, новый интерфейс передачи указателей, похоже, решает все проблемы безопасности, связанные с передачей значений указателей между расширениями в SQLite.
3.1. Типы указателей
"Тип указателя" в последнем параметре sqlite3_bind_pointer(), sqlite3_result_pointer() и sqlite3_value_pointer() используется для предотвращения перенаправления указателей, предназначенных для одного расширения, к другому расширению. Например, без использования типов указателей злоумышленник по-прежнему мог получить доступ к информации об указателях в системе, включающей как расширение FTS3, так и CARRAY, используя SQL-код такого типа:
SELECT ca.value FROM t1, carray(t1,10) AS ca WHERE cx MATCH $pattern
В приведенном выше операторе указатель курсора FTS3, сгенерированный оператором MATCH, передаётся в функцию с табличным значением carray() вместо предназначенного для него получателя snippet(). Функция carray() обрабатывает указатель как указатель на массив целых чисел и возвращает каждое целое число по одному, тем самым утекая содержимое объекта курсора FTS3. Поскольку объект курсора FTS3 содержит указатели на другие объекты, указанный оператор является утечкой указателя.
Однако, приведенный выше оператор не работает благодаря типам указателей. Указатель, сгенерированный оператором MATCH, имеет тип "fts3cursor", но функция carray() ожидает получения указателя типа "carray". Поскольку тип указателя в вызове sqlite3_result_pointer() не совпадает с типом указателя в вызове sqlite3_value_pointer(), sqlite3_value_pointer() возвращает NULL в carray() и тем самым сигнализирует расширению CARRAY, что ему был передан неверный указатель.
3.1.1. Типы указателей — статические строки
Типы указателей — это статические строки, которые в идеале должны быть строковыми литералами, встроенными непосредственно в вызов API SQLite, а не параметрами, переданными из других функций. Было рассмотрено использование целочисленных значений в качестве типа указателя, но статические строки предоставляют гораздо более обширное пространство имён, что снижает вероятность случайных коллизий имён типов между не связанными расширениями.
Под «статической строкой» мы подразумеваем нуль-терминированную последовательность байтов, которая является фиксированной и неизменной на протяжении всего жизненного цикла программы. Другими словами, строка типа указателя должна быть константой строки. В противоположность этому, «динамическая строка» — это нуль-терминированная последовательность байтов, которая хранится в памяти, выделенной из кучи, и которую необходимо освободить, чтобы избежать утечки памяти. Не используйте динамические строки в качестве строки типа указателя.
Несколько комментаторов выразили желание использовать динамические строки для типа указателя и дать SQLite права собственности на строки типа, а также автоматически освободить строку типа, когда SQLite закончит её использование. Данный дизайн отклоняется по следующим причинам:
Тип указателя не предназначен для гибкости и динамичности. Тип указателя предназначен для константы на этапе проектирования. Приложения не должны синтезировать строки типа указателя во время выполнения. Поддержка динамических строк типа указателя приведет к неправильному использованию интерфейсов передачи указателей разработчиками путём создания синтезированных строк типа указателя во время выполнения. Требование, чтобы строки типа указателя были статическими, побуждает разработчиков делать правильный выбор, выбирая фиксированные имена типов указателей на этапе проектирования и кодируя эти имена как константные строки.
Все строковые значения на уровне SQL в SQLite — это динамические строки. Требование, чтобы строки типа были статическими, затрудняет создание пользовательской SQL-функции, которая может синтезировать указатель произвольного типа. Мы не хотим, чтобы пользователи создавали такие SQL-функции, поскольку такие функции могут нарушить безопасность системы. Таким образом, требование использовать статические строки помогает защитить целостность интерфейсов передачи указателей от плохо спроектированных SQL-функций. Требование статических строк не является идеальной защитой, поскольку опытный программист может обойти его, а неопытный программист может просто принять утечку памяти. Но, заявив, что строка типа указателя должна быть статической, мы надеемся побудить разработчиков, которые могли бы в противном случае использовать динамическую строку для типа указателя, более тщательно подумать над проблемой и избежать возникновения проблем с безопасностью.
Предоставление SQLite прав собственности на строки типа наложит дополнительные затраты на производительность на все приложения, даже на приложения, которые не используют интерфейсы передачи указателей. SQLite передаёт значения как экземпляры объекта sqlite3_value. Этот объект имеет деструктор, который, учитывая тот факт, что объекты sqlite3_value используются почти для всего, вызывается часто. Если деструктору нужно проверить, есть ли строка типа указателя, которую нужно освободить, это несколько дополнительных циклов CPU, которые нужно потратить на каждый вызов деструктора. Эти циклы накапливаются. Мы были бы готовы понести дополнительные затраты на циклы CPU, если передача указателей была распространённой парадигмой программирования, но передача указателей редка, и поэтому кажется неразумным накладывать затраты на производительность на миллиарды приложений, которые не используют передачу указателей, только для удобства нескольких приложений, которые это делают.
Если вы считаете, что в вашем приложении необходимы динамические строки типа указателя, это чёткий признак того, что вы неправильно используете интерфейс передачи указателей. Ваше предполагаемое использование может быть небезопасным. Пожалуйста, пересмотрите свой дизайн. Определите, действительно ли вам нужно передавать указатели через SQL. Или, возможно, найдите другой механизм, отличный от интерфейсов передачи указателей, описанных в этой статье.
3.2. Функции-деструкторы
Последний параметр процедур sqlite3_bind_pointer() и sqlite3_result_pointer() — указатель на процедуру, используемую для утилизации указателя P после того, как SQLite закончит с ним. Этот указатель может быть NULL, в этом случае деструктор не вызывается.
Когда параметр D не равен NULL, это означает, что право собственности на указатель передаётся SQLite. SQLite возьмёт на себя ответственность за освобождение ресурсов, связанных с указателем, когда закончит использовать его. Если параметр D равен NULL, это означает, что право собственности на указатель остаётся у вызывающей стороны, и она несёт ответственность за утилизацию указателя.
Обратите внимание, что функция-деструктор D предназначена для значения указателя P, а не для строки типа T. Строка типа T должна быть статической строкой с бесконечным жизненным циклом.
Если право собственности на указатель передаётся SQLite путём указания параметра D, отличного от NULL, в sqlite3_bind_pointer() или sqlite3_result_pointer(), то право собственности остаётся у SQLite до уничтожения объекта. Нет возможности передать право собственности из SQLite обратно в приложение.
4. Ограничения на использование значений указателей
Указатели, которые «присоединяются» к SQL-значениям NULL с помощью интерфейса sqlite3_bind_pointer(), sqlite3_result_pointer() и sqlite3_value_pointer(), являются временными и эфемерными. Указатели никогда не записываются в базу данных. Указатели не переживут сортировку. Последний факт является причиной отсутствия интерфейса sqlite3_column_pointer(), так как невозможно предсказать, вставит ли планировщик запросов операцию сортировки перед возвращением значения из запроса, поэтому невозможно знать, переживёт ли значение указателя, вставленное в запрос с помощью sqlite3_bind_pointer() или sqlite3_result_pointer(), до конечного набора результатов.
Значения указателей должны передаваться непосредственно от производителя к потребителю без промежуточных операторов или функций. Любая трансформация значения указателя уничтожает указатель и преобразует значение в обычное SQL-значение NULL.
5. Заключение
Основные выводы из этого эссе:
Интернет — всё более враждебное место. В наши дни разработчики должны исходить из того, что злоумышленники найдут способ выполнить произвольный SQL-запрос в приложении. Приложения должны быть спроектированы так, чтобы предотвратить выполнение произвольных SQL-запросов от эскалации в более серьёзную атаку.
-
Несколько расширений SQLite выигрывают от передачи указателей:
- Оператор MATCH FTS3 передаёт указатели в snippet(), offsets() и matchinfo().
- Функция с табличным значением carray должна принимать указатель на массив значений языка C из приложения.
- Расширение remember() нуждается в указателе на переменную целого типа языка C, в которой следует сохранить значение.
- Приложения должны получить указатель на объект "fts5_api", чтобы добавить расширения, такие как пользовательские токенизаторы, к расширению FTS5.
Указатели никогда не должны обмениваться, кодируясь в качестве другого SQL-типа данных, такого как целые числа или BLOB. Вместо этого используйте интерфейсы, разработанные для обеспечения безопасной передачи указателей: sqlite3_bind_pointer(), sqlite3_result_pointer() и sqlite3_value_pointer().
Использование передачи указателей — это продвинутая техника, которую следует использовать редко и с осторожностью. Передачу указателей не следует применять хаотично или бездумно. Передача указателей — это острый инструмент, который может оставить глубокие раны при неправильном использовании.
Строка «типа указателя», которая является последним параметром каждого из интерфейсов передачи указателей, должна быть отличительной, специфичной для приложения строкой-литералом, которая появляется непосредственно в вызове API. Тип указателя не должен быть параметром, переданным из функции более высокого уровня.
SQLite is in the Public Domain.
https://sqlite.org/bindptr.html