Comments 153
Вообще, занятно: получается, что проблема во всех этих инъекциях из-за того, что мы передаём код на языке SQL в виде строки, а не в виде AST. Если, скажем, формировать запрос в виде:
request = Select
[AllColumns] -- *
Just (EqExpr (Var "name")
(StrLiteral "Robert'); DROP TABLE users;"))
Nothing -- order by
и инжектировать прямо в движок базы данных, минуя парсер базы данных, то эти SQL инъекции автоматом станут безопасны.
Именно это и делают плейсхолдеры, когда prepared statements поддерживаются на уровне базы
Я не сомневался, что идею передачи AST в базу данных уже кто-то реализовал. Но, как обычно, назвал своим личным термином.
Но я к тому, что надо бы S-expression ввести в качестве универсального формата общения разных современных ЯВУ. И передавать, разумеется, не в строковом, а бинарном виде. А сейчас всё делается через C.
Все-таки, подготовленные выражения — это не совсем AST. Или даже совсем не. Это именно что плейсхолдеры, переменные только для данных.
надо бы S-expression ввести в качестве универсального формата общения разных современных ЯВУ. И передавать, разумеется, не в строковом, а бинарном виде.
А какая, собственно, разница? Вам все равно надо как-то в этот массив байт подставить значение, введенное пользователем.
$name= $request->get('name');
Вот есть у вас переменная с нужным значением, а дальше что? Тут проблема не в том, AST это или нет, а в том, что в него подставляются (конкатенируются) данные, которые пришли извне. База все равно будет его как-то интерпретировать и выполнять, хоть с парсингом, хоть без. А именованные переменные это такой механизм протокола обмена, что база знает, что в этом месте пакета данных лежат только данные SQL-запроса, и их не надо выполнять как код.
Вариант с S-выражениями подходит для любых DSL, а не только для SQL. В SQL-то задача решена, по-сути.
Вы ответьте на вопрос пожалуйста. У вас есть переменная из запроса, как вы будете вставлять ее в запрос к БД в виде S-выражения? То есть заполните многоточие в этом примере:
$name = $request->get('name');
$sExpr = "...";
...
$results = $db->getResults($sExpr);
Ну непосредственно. А почему у вас S-выражение закавычено?
Приведите пожалуйста пример на языке программирования. Потом можно будет обсудить более предметно.
Наверху я написал на Хаскеле.
Там нет использования переменной из входных данных и отправки запроса в БД. Там просто константный запрос, для которого проблема SQL-инъекции в принципе не существует. Также непонятно, как эта структура из набора рантайм-объектов с каким-то адресами в оперативной памяти должна передаваться по сети на другой компьютер.
Также непонятно, как эта структура из набора рантайм-объектов с каким-то адресами в оперативной памяти должна передаваться по сети на другой компьютер.
Разговор гипотетический, вообще-то. Цитирую себя:
"и инжектировать прямо в движок базы данных, минуя парсер базы данных, то эти SQL инъекции автоматом станут безопасны."
Поэтому вопрос конкретной реализации не стоит. То что, в принципе можно реализовать — очевидно.
Понятно, рассуждения из серии "Я не тактик, я стратег"
То что, в принципе можно реализовать — очевидно.
Нет. Потому что как я уже сказал, проблема не в том, в чем вы думаете, соответственно, предложенное вами решение ее не убирает.
Разговор гипотетический, вообще-то.
Если разговор о том, что можно реализовать, то как раз не гипотетический, а практический.
Цитирую себя:
"и инжектировать прямо в движок базы данных, минуя парсер базы данных"
Это утверждение не содержит ответа на мой вопрос. Более того, я прямо на это указал в первом сообщении.
Код работает на одном сервере, база на другом, поэтому без передачи по сети обойтись принципиально невозможно. А это порождает вопросы, которые я задал. У вас есть на них ответ?
Блин, ну сериализацию с десериализацией ещё в прошлом веке придумали же...
Так SQL-инъекция и возникает на этапе сериализации, когда вы конкатенируете туда рантайм-строку. Без эскейпинга будет такая же S-expr-инъекция. А с эскейпингом это ничем не отличается от обычного SQL.
Я конкатенирую?
Без экранирования это не сериализация, а хрень. Очевидно же, что использование некоторого универсального формата предполагает использование и универсального сериализатора, у которого не должно быть глупых ошибок. В пользовательском коде при формировании запроса не должно быть операций конкатенации строк в принципе.
"Вы", который пишет сериализацию из рантайм-объектов.
Ну дак и так же очевидно, что зачем тогда в этой схеме нужен специальный S-expression? Query Builder для SQL с автоматическим экранированием даст такой же результат. О чем я и говорю, нет разницы, что использовать в качестве языка сериализации, SQL или не SQL.
Только вот Query Builder — это довольно сложная конструкция, а удобный Query Builder — ещё более сложная.
Так что вполне понятно желание использовать более общий механизм, странно что вы не понимаете.
Так для построения AST SQL из языка программирования вам тоже нужен специальный билдер. Странно что вы этого не понимаете.
Не уверен что набор конструкторов достоин называться билдером. Но даже если мы его так назовём — он будет значительно проще чем предназначенный для формирования SQL.
Набор конструкторов даст вам рантайм-объект, а не сформированные сериализованные данные. Как и объект Query сам по себе не даст вам строку SQL.
Не знаю, почему вы говорите что он будет проще, сложность у них одинаковая.
(new Query())
->select('*')
->from('talbe')
->where(['=', 'field', 'value'])
(new Query(
new Select('*'),
new From('table'),
new Where(new Eq('field', 'value')),
))
Дальше вам надо передать этот объект по сети, и тут в обоих случаях появляется код, который сериализует их в строку. Покажите пожалуйста, где вы тут видите значительную разницу.
Тогда уж new Eq(new Field('field'), new Literal('value'))
.
Принципиальная разница при сериализации тут в том, что при втором способе все строки будут писаться строго одинаково (и, вероятно, одной подпрограммой). В случае же QB для SQL часть строк надо заключать в двойные кавычки, часть в одинарные, к ним надо применять разные правила экранирования и т.п.
при втором способе все строки будут писаться строго одинаково (и, вероятно, одной подпрограммой)
Query Builder это тоже одна подпрограмма. Query Builder, сериализующий рантайм-обект в строку байт с SQL внутри, это полный аналог любого другого сериализатора, сериализующего рантайм-объект в строку байт с другим языком внутри. Я не знаю, где вы тут видите разницу. Есть язык, есть правила сериализации в него. Следуете правилам, не будет инъекции, не следуете, будет.
Подход "сдампить всю память дерева" при хранениии строк в C-style будет работать, но не по причие "строки пишутся одинаково", а из-за использования указателей на отдельные части запроса. Указатель обозначает "данные этого типа начинаются отсюда". Возможность инъекций при других подходах возникает из-за того, что обычно в форматах сериализации не используют указатели на начало отдельных частей для экономии места, считается, что следующая часть структуры запроса начинается после того, как закончилась предыдущая. Именно поэтому, когда символ окончания строки не экранирован, десериализатор заканчивает строку раньше и начинает интерпретировать следующие данные как другие части структуры. При дампе всей структуры указатели будут, в каждом узле будет список указателей на дочерние элементы, десериализатор будет проходить по нему, и следущий элемент после строкового литерала возьмет из этого списка. Остаток строки после неэкранированного завершающего символа в середине будет просто проигнорирован.
В случае же QB для SQL часть строк надо заключать в двойные кавычки, часть в одинарные
Во-первых, функции экранирования находятся в дрйвере и сами ставят нужные кавычки вокруг строки. Строковые значения всегда пишутся одинаково, в соответствии с синтаксисом конкретного диалекта.
Во-вторых, сериализаторы для QB SQL, по крайней мере в PHP, уже давно умеют использовать именованные параметры вместо экранирования. Строки в этом случае передаются как "длина+данные", я приводил ссылку на документацию MySQL. Тоже строго одинаково. При этом это все равно SQL и Query Builder.
Во-первых, функции экранирования находятся в дрйвере и сами ставят нужные кавычки вокруг строки. Строковые значения всегда пишутся одинаково, в соответствии с синтаксисом конкретного диалекта.
…и их нужно не забыть вызвать, к тому же не перепутав.
Во-вторых, сериализаторы для QB SQL, по крайней мере в PHP, уже давно умеют использовать именованные параметры вместо экранирования.
Только для значений, но не для имён полей.
и их нужно не забыть вызвать, к тому же не перепутав.
Вы все-таки не понимаете, что такое Query Builder. Он сам автоматически вызывает экранирование данных если нужно, это его прямое назначение, с ним вы не вызываете экранирование сами в своем коде. При этом экранирование используется для конструкций типа LIKE '%...%'
, в остальных случаях используются именованные параметры.
Только для значений, но не для имён полей.
Имена полей и не приходят в пользовательском запросе, их контролирует программист, зачем их передавать через именованные параметры.
Вы все-таки не понимаете, что такое Query Builder. Он сам автоматически вызывает [...]
А, извиняюсь. Вы где-то тут в обсуждении напирали на сложности сериализации, вот я и писал с точки зрения реализации Query Builder.
Если считать Query Builder уже реализованным — тут, конечно же, разницы при сериализации никакой не будет. Однако, тут значимым становится другое отличие — в API.
В случае с Query Builder вы можете обойти его и просто передать голый SQL (с инъекцией, разумеется) драйверу СУБД.
В варианте с выражениями драйвер СУБД должен принимать это самое выражение. Места, куда можно ошибочно вставить голый запрос, просто нет.
вот я и писал с точки зрения реализации Query Builder
В коде реализации Query Builder будет несколько заранее известных мест, которые обрабатываются универсально. Это делается один раз при написании Query Builder. Фраза "забыть вызвать, к тому же не перепутав" выглядит как будто она относится к составлению конкретного запроса в приложении.
В случае с Query Builder вы можете обойти его и просто передать голый SQL (с инъекцией, разумеется) драйверу СУБД.
Ну и с AST вы можете записать произвольные данные в порт в обход драйвера. Более того, скорее всего в составе библиотеки, которая предлагает AST, будет отдельный класс, который пишет результат сериализации в сеть в соответствии с протоколом БД, и его можно использовать.
В обоих случаях у вас есть библиотечный компонент, который писали не вы, и который принимает на вход какой-то объект, и в обоих случаях вы можете его не использовать, а использовать что-то другое, и это будет заметно только на код-ревью.
Места, куда можно ошибочно вставить голый запрос, просто нет.
Передать вручную сформированную строку с SQL в обход ORM/QB можно только намеренно, это требует много намеренных действий, случайно ошибиться нельзя.
Передать вручную сформированную строку с SQL в обход ORM/QB можно только намеренно, это требует много намеренных действий, случайно ошибиться нельзя.
Зато можно просто не знать про существование ORM/QB, подавляющее большинство инъекций именно так и возникает. Просто программист учился по устаревшим учебникам.
Вы как-то очень странно поменяли свою риторику. В соседней ветке критиковали идею этого "универсального формата", а теперь он уже в ваших собственных рассуждениях появился.
Так SQL-инъекция и возникает на этапе сериализации, когда вы конкатенируете туда рантайм-строку.
Сериализация-сериализации рознь. Когда вы передаёте дерево AST, вставляя строки в качестве узлов этого дерева, эти строки сериализуются отдельно и крайне примитивно (фактически копируются как есть). А дерево может сериализоваться вообще отдельно, как в PreparedStatement.
строки сериализуются отдельно и крайне примитивно (фактически копируются как есть).
Если вы будете сериализовывать строковое значение, запишете в поток байт открывающую кавычку, потом скопируете данные из оперативной памяти как есть, потом закрывающую кавычку, у вас будет Expr-инъекция.
А дерево может сериализоваться вообще отдельно, как в PreparedStatement
Ну так я про это и говорю, PreparedStatement (когда переменные передаются отдельно от запроса) решает проблему, использование какого-то другого языка без PreparedStatement нет. С PreparedStatement нет разницы, использовать SQL или что-то еще.
Если вы будете сериализовывать строковое значение, запишете в поток байт открывающую кавычку, потом скопируете данные из оперативной памяти как есть, потом закрывающую кавычку, у вас будет Expr-инъекция.
Ясень пень, нет.
Можно взять, скажем, C-шные строки и записать строку как есть, можно (и лучше), взять Паскалевские строки, записав сперва длину строки в байтах, а потом содержимое. Можно вообще сжать каким-то алгоритмом и записать сжатое.
Можно просто сделать дамп куска памяти, в котором находится это дерево, если оно аллоцировано в непрерывной арене. Только указатели придётся промаркировать.
Ну мало ли разных вариантов однозначной сериализации, причём тут вообще кавычки?
Ладно, я утомился от этой бессмыслицы. Давайте закроем дискуссию.
причём тут вообще кавычки?
При том, что в любом формате вам надо как-то обозначать начало и конец строки для десериализатора.
Можно взять, скажем, C-шные строки и записать строку как есть
Нельзя. Пользователь контролирует содержимое строки и может записать туда в середину байт 0, а остальное заполнить кодом в вашем формате. Если вы в оперативной памяти приложения работаете со строками как "указатель+длина" и записываете заданное количество байт, а база данных десериализует данные как C-строки, то будет инъекция. База встретит 0, посчитает что строка закончилась, и будет интерпретировать следующие данные как продолжение S-Expr.
Байт 0 в этом плане ничем не отличается от кавычек. В обоих случаях у сериализатора есть правило "считай строкой все байты, пока не встретишь байт X, после него снова интерпретируй данные как код".
взять Паскалевские строки, записав сперва длину строки в байтах, а потом содержимое
Это можно, так будет работать. Только это вариация именованных параметров, когда они объявляются не где-то отдельно после текста запроса, а in-place, без указания собственно имени параметра. Мы фактически говорим базе данных "отсюда досюда находятся данные для запроса".
Для этого не нужно какое-то специальное AST, достаточно модифицировать SQL и отправлять его вот так.
SELECT * FROM user
WHERE name = (str:18)user OR name=admin
В этом случае SQL-инъекции не будет, база будет искать пользователя с именем "user OR name=admin".
Это можно, так будет работать.
[Крестится] Ну слава б-гу!
В этом случае SQL-инъекции не будет, база будет искать пользователя с именем "user OR name=admin".
Ура! Ура! Ура!
Хоть на чём-то согласились. Да, С-шные строки, разумеется, придётся санировать перед сериализацией, что в Сшной же инфраструктуре делается трахтоматом — они будут обрезаны ещё на входе в backend первым же strcpy
.
Ну слава б-гу!
Что слава б-гу? Вы говорили, что AST решает все проблемы с инъекциями, а это не так. Решает правильная передача данных, а не AST, о чем я сразу сказал. AST тут вообще не нужен, пример я привел.
что в Сшной же инфраструктуре
При чем тут Сшная инфраструктура? На других языках нельзя работать с базой данных?
Если вы будете сериализовывать строковое значение, запишете в поток байт открывающую кавычку, потом скопируете данные из оперативной памяти как есть, потом закрывающую кавычку, у вас будет Expr-инъекция.
Скажите, вот вы, судя по публикациям, на PHP пишете (либо писали в прошлом). Как часто вы писали свою реализацию вот этих функций и зачем вы это делали?
https://www.php.net/manual/en/function.json-encode.php
https://www.yiiframework.com/doc/api/2.0/yii-helpers-json#encode()-detail
PS знаете, а я ведь только что придумал "отличный" аргумент против параметризованных запросов.
Вы, когда будете писать параметризацию, запишете в поток байт открывающую кавычку, потом скопируете данные из оперативной памяти как есть, потом закрывающую кавычку, у вас будет параметрическая инъекция.
Согласитесь, бред? Мы как-то предполагаем, что уж что-что, а механизм параметризации будет использоваться готовый и не имеющий подобных идиотских багов. Так почему вы S-выражениям отказываете в праве на готовый и не имеющий багов способ сериализации?
Как часто вы писали свою реализацию вот этих функций и зачем вы это делали?
json_encode
Вы не следите за дискуссией. Было утверждение "взять, скажем, C-шные строки и записать строку как есть", я отвечал на него. Вы утверждаете, что json_encode пишет строки как есть? Это неправда, json_encode экранирует кавычки в строках в соответствии с правилами JSON-формата.
Мы как-то предполагаем, что уж что-что, а механизм параметризации будет использоваться готовый и не имеющий подобных идиотских багов.
Естественно, и в механизме параметризации используется схема "тип данных + длина", а не завершающий символ. Иначе бы была такая же проблема с экранированием.
Binary Protocol Value
MYSQL_TYPE_STRING
string<lenenc> value String
Example
03 66 6f 6f -- string = "foo"
Так почему вы S-выражениям отказываете в праве на готовый и не имеющий багов способ сериализации?
Вы не следите за дискуссией. Человек сказал, что проблема из-за того, что код передается в виде строки, и чтобы решить проблему с инъекциями, надо передавать в виде AST. Я сказал, что это не так, использование AST без экранирования или специального обозначения данных проблему не решит, и от передачи SQL ничем не отличается, а если их использовать, тогда AST не нужен.
Ни про что готовое я не говорил, ни про S-выражения, ни про SQL. Мы говорим про способы реализации безопасной передачи запроса с данными в БД. Поэтому ваша претензия о том, что эту реализацию написал кто-то другой неизвестно как, но она точно работает, нерелевантна.
Вы не следите за дискуссией. Было утверждение "взять, скажем, C-шные строки и записать строку как есть"
Это вы не уследили. Очевидно же, что если на одной строке нуль-терминированная строка пишется как есть — то и на другой стороне она будет читаться как есть. До нулевого символа, а не до кавычки.
Я сказал, что это не так, использование AST без экранирования или специального обозначения данных проблему не решит, и от передачи SQL ничем не отличается, а если их использовать, тогда AST не нужен.
Так вот, вам тут пытаются объяснить что S-выражение (не AST!) от текстового SQL всё-таки отличается. Хотя бы тем, что мы не ограничены форматом сериализации и можем выбрать более удобный. Например, бинарный.
Никто не предлагает генерировать из S-выражения снова SQL, напротив, с самого начала предлагалось
инжектировать прямо в движок базы данных, минуя парсер базы данных
Очевидно же, что если на одной строке нуль-терминированная строка пишется как есть — то и на другой стороне она будет читаться как есть. До нулевого символа, а не до кавычки.
Приведите пожалуйста цитату, с которой вы спорите. Потому что я нигде не говорил, что нуль-терминированная строка будет читаться до кавычки. Это 2 разных способа передачи — от кавычки до кавычки, или от указателя на начало до символа 0. Только логически они эквивалентны, и в обоих при определенных условиях может быть инъекция.
В PHP не используются нуль-терминированные строки, там всегда указывается длина, поэтому в середине вполне может быть байт 0. А на другой стороне да, она будет читаться до нулевого символа, а дальше десериализатор посчитает, что строка закончилась, и следующие байты уже не будет интерпретировать как часть этой строки.
Никто не предлагает генерировать из S-выражения снова SQL
Приведите пожалуйста цитату, где я говорю про генерацию SQL из S-выражения.
Так вот, вам тут пытаются объяснить что S-выражение (не AST!) от текстового SQL всё-таки отличается.
можем выбрать более удобный. Например, бинарный
Это я вам объясняю, что от формата сериализации возможность инъекций не зависит, зависит только от способа сериализации строк в этот формат. Бинарный формат не означает отсутствие инъекций. Я же выше привел вполне себе текстовый формат без инъекций.
Если вы будете "писать строки как есть" в бинарный формат, то тоже будет инъекция, так же как если "писать строки как есть" в текстовый. Предотвращение инъекций заключается только в том, как правильно обозначить начало и конец строкового значения, независимо от того, какие байты его окружают.
Если в формате сериализации строки обозначаются как "от кавычки до кавычки", значит при сериализации строки надо экранировать кавычки внутри нее, если как "отсюда до байта 0", значит надо при сериализации экранировать байты 0 внутри нее, если как "отсюда длиной X", то экранировать ничего не надо. Независимо от того, что находится в остальной части пакета, текстовый SQL или бинарный Protobuf.
Приведите пожалуйста цитату, с которой вы спорите. Потому что я нигде не говорил, что нуль-терминированная строка будет читаться до кавычки.
Вот же оно:
Если вы будете сериализовывать строковое значение, запишете в поток байт открывающую кавычку, потом скопируете данные из оперативной памяти как есть, потом закрывающую кавычку
Вот с этой цитатой я и спорю.
Приведите пожалуйста цитату, где я говорю про генерацию SQL из S-выражения.
Прямо вы этого не говорили, но алгоритм из вашей цитаты выше имеет смысл только при попытке сгенерировать SQL.
Только логически они эквивалентны, и в обоих при определенных условиях может быть инъекция.
Ага, может. Только вот в одном случае инъекция может быть в любом из мест формирования запроса, а в другом — лишь в коде сериализации. Ровно в одном месте во всей программе, которое к тому же скорее всего находится в библиотеке.
Вот совсем никакой разницы?
— Когда вы передаёте дерево AST, вставляя строки в качестве узлов этого дерева, эти строки сериализуются отдельно и крайне примитивно
— Если вы будете сериализовывать строковое значение, запишете в поток байт открывающую кавычку, потом скопируете данные из оперативной памяти как есть, потом закрывающую кавычку, у вас будет Expr-инъекция.
— Можно взять, скажем, C-шные строки и записать строку как есть, можно взять Паскалевские строки.
— C-шные строки взять нельзя. Пользователь контролирует содержимое строки и может записать туда в середину байт 0. Есть языки, которые работают со строками в Паскаль-стиле, в них это возможно.
— Как часто вы писали свою реализацию json_encode?
— При чем тут json_encode? Она не пишет строки как есть. Я отвечал про "записать строку как есть".
— Нуль-терминированная строка будет читаться до нулевого символа, а не до кавычки.
— Я не говорил, что нуль-терминированная строка будет читаться до кавычки.
— Здесь говорили: "запишете в поток байт открывающую кавычку, потом закрывающую..."
Извините, я не понимаю, как в этом диалоге ваши ответы связаны с моими.
В начале я привел пример с форматом сериализации дерева AST, где строки обозначаются кавычками (как например в JSON). В этом случае писать содержимое строки "как есть" нельзя. Нуль-символ к содержимому строки не относится, это маркер окончания содержимого, и я не предлагал его писать в данные между кавычками.
Потом мой собеседник привел в пример C-шные строки, и я сказал, что в них тоже надо экранировать байт 0 в тех языках, которые работают со строками в Паскаль-стиле. Это 2 разных ситуации, в одной кавычки, в другой 0. Но экранировать эти символы в содержимом строки надо в обоих.
Вы, видимо, подразумеваете, что "писать строку как есть" это вместе с этим символом. Но по условиям моего примера формат сериализции дерева AST подразумевает кавычки для строк (как JSON), и читать в десериализаторе JSON от открывающей кавычки до нулевого символа выглядит вообще бессмысленно.
Не надо экранировать только если мы явно указываем или длину строк или позицию начала следующего токена запроса после строки (дамп памяти с указателями, пересчитанными в смещение от начала данных в пакете). При этом во втором случае строка тоже будет распознана неправильно, просто это не приведет к инъекции.
алгоритм из вашей цитаты выше имеет смысл только при попытке сгенерировать SQL.
"использование AST без экранирования или специального обозначения данных проблему не решит, и от передачи SQL ничем не отличается, а если их использовать, тогда AST не нужен"
Я тут не говорил и не подразумевал генерацию SQL из S-выражения. Я говорил, что эти 2 способа "сериализация объекта AST в поток байт" и "сериализация объекта QB в SQL" ничем не отличаются, в обоих случаях можно получить инъекцию, если не экранировать данные или не указывать их точное расположение.
Только вот в одном случае инъекция может быть в любом из мест формирования запроса, а в другом — лишь в коде сериализации.
В объекте Query Builder для SQL инъекции по определению быть не может, в нем еще нет никакого SQL. Вы похоже не понимаете, что преобразование объекта QB в пакет данных с SQL-запросом это и есть сериализация, и происходит она только перед отправкой по сети, а не в месте формирования запроса. В местах формирования запроса меняются только поля в объекте QB.
Вот совсем никакой разницы?
Да. Потому что с Query билдером то же самое, его именно для этого и придумали.
Там у вас в спойлере не все сообщения мои. Если что, у меня нет виртуалов на Хабре. И да, похоже что ветка общими усилиями несколько запуталась.
Если начать с начала, то есть два принципиально разных варианта сериализации дерева. Либо использовать что-то стандартное, либо самопал.
В первом случае предполагается, что нужный код уже написан, и никто не будет вставлять строку "как есть" между кавычек. Сюда же относился и вопрос про то, как часто вы пишете аналог json_encode.
Второй вариант предполагает, что программист сам будет писать сериализацию. Сюда и относились бинарные форматы, в которые строки можно вставлять как есть. Использовать второй вариант предпочтительнее если программа будет писаться на Си (ведь именно сишники и плюсовики знамениты своими костылями вместо библиотек), отсюда и упоминание нуль-терминированных строк. Кстати, писать в файл нуль-терминированную строку как есть безопасно если исходная строка в памяти тоже нуль-терминирована, а это типичная ситуация для сишки (пхпшник же просто выберет формат с json).
А вообще, вся эта дискуссия бесполезна, поскольку мы все трое как-то упустили тот факт, что в 99,9% случаев сериализацией будет заниматься драйвер СУБД, а не прикладная программа. И принципиальное отличие QB от S-выражений именно в этом.
Там у вас в спойлере не все сообщения мои.
Я в курсе, но я же отвечал в контексте разговора. Вы в ответах говорили про нуль-символы, но начали про них говорить не вы.
Либо использовать что-то стандартное, либо самопал.
Нет. Я же говорю, вы не следите за дискуссией. Или неправильно ее поняли. Если начать с начала, разговор был про то, как сделать передачу запроса без инъекций. То есть в ваших терминах только про "самопал". Рассматривались 2 варианта реализации — с AST и с SQL. И было утверждение, что дескать использование в приложении AST вместо SQL решит проблему с инъекциями. Я привел аргументы, что нет, и для SQL и для AST нужно делать правильный сериализатор, который будет предотвращать инъекции, поэтому разницы между этими подходами нет.
поскольку мы все трое как-то упустили тот факт, что в 99,9% случаев сериализацией будет заниматься драйвер СУБД, а не прикладная программа
Мы не упустили из виду, а говорим о том, как он должен работать. О том, как безопасно передать запрос в БД, независимо от того, где находится реалиазция.
Сюда и относились бинарные форматы, в которые строки можно вставлять как есть.
Ну одно да потому. Нет, если вы будете вставлять в бинарный формат сериализации строку как есть, то у вас в некоторых форматах все равно будет инъекция. "Некоторые" это в которых следующий токен начинается после завершения предыдущего, то есть практически любые распространенные.
// эквивалент SELECT * FROM users WHERE login = 'user' OR email = 'user'
// 01 - SELECT, 02 - FROM, 03 - WHERE, 11 - OR, 12 - EQ, 21 - field, 22 - string
// скобки и кавычки просто для наглядности
01 '*\0' 02 'users\0' 03 (11 (12 21 'login\0' 22 'user\0') (12 21 'email\0' 22 'user\0'))
// расшифровка для понятности
SE '*\0' FR 'users\0' WH (OR (EQ FI 'login\0' ST 'user\0') (EQ FI 'email\0' ST 'user\0'))
Теперь берем PHP, в котором строки в Pascal-style, и используем библиотеку с AST, которая пишет строки как есть.
$name = $request->get('name');
// name = "user%00%12%21role%00%22admin%00"
// name = "'user\0' (EQ FI 'role\0' ST 'admin\0')"
$user = findUserByLoginOrEmail($name);
В сеть отправятся такие данные:
01 '*\0' 02 'users\0' 03 (11 (12 21 'login\0' 22 'user\0') (12 21 'role\0' 22 'admin\0') (12 21 'email\0' 22 'user\0'))
SE '*\0' FR 'users\0' WH (OR (EQ FI 'login\0' ST 'user\0') (EQ FI 'role\0' ST 'admin\0') (EQ FI 'email\0' ST 'user\0'))
// эквивалент SELECT * FROM users WHERE login = 'user' OR role = 'admin' OR email = 'user'
Вполне себе инъекция.
А это порождает вопросы, которые я задал. У вас есть на них ответ?
Это порождает практические вопросы, а я не хочу вдаваться в детали практики.
Вообще, если уж на то пошло, мне кажется что в теории это реализовать можно. Это будет такой аналог квери билдера, в котором операторы SQL выступают в виде функций, а динамические значения передаются в них в качестве аргументов. Только потом этот квери билдер не генерирует SQL, а отправляет всё как есть, "деревом".
Но, конечно, идея такого АПИ с "внекишечным пищеварением" вряд ли реализуема на практике.
а отправляет всё как есть, "деревом"
Так вот проблема появляется как раз в момент отправки, когда вы конкатенируете рантайм-значения по разным адресам в один поток байтов. Если делать это неправильно, то получится такая же инъекция. А если правильно, то от SQL это не отличается, что сериализованное дерево это массив байтов, что SQL.
От SQL "с конкатенацией" это отличается. Суть инъекции ведь в том, что мы пользовательский ввод передаем не в параметры нашего "квери билдера", а включаем прямо в его код. Перемешиваем код и данные.
А вот от составления параметризованного запроса не отличается, да.
Фактически, получается что мы тут обсуждаем, кто делает сериализацию — человек или машина. То есть все сводится к устранению человеческого фактора :)
а включаем прямо в его код. Перемешиваем код и данные.
Ну так и с деревом, если его неправильно сериализовывать, будет то же самое. Сериализованные данные это и есть строка с кодом.
Да. Поэтому вопрос не в том, сериализовать или не сериализовать, а в том, кто будет это делать. Как нам уже намекал товарищ Майоров (а до меня дошло только сейчас), машинная сериализация вопросов обычно не вызывает. Перекладывая сериализацию на машину, будь то привычные подготовленные выражения, или воображаемое дерево, о котором здесь идет речь, мы как раз и избегаем инъекции. Которая, в конечном итоге, сводится к человеческому фактору.
Человеческий фактор при составлении запроса тут ни при чем. Если сериализация написана неправильно, у вас все равно будут инъекции, несмотря на то, что она делается автоматически.
машинная сериализация вопросов обычно не вызывает
Так она не вызывает потому что либо экранирует данные, либо указывает длину данных. Какое-то специальное AST тут не играет никакой роли. То же самое можно достичь с query-билдером для обычного SQL.
То же самое можно достичь с query-билдером для обычного SQL.
Чтобы этого достичь с query-билдером для обычного SQL, надо сначала обязать всех использовать query-билдер. А он необязателен, API ничуть не мешает передавать произвольную строку.
API чего, сервера БД? Оно не мешает и вместо AST на стороне приложения записать произвольные данные. И даже если сделать клиентскую библиотеку, которая принимает на вход только объект AST и правильно его сериализует, программист может подключиться к порту БД напрямую и отправить что угодно. Обязать его использовать AST и этот клиент программными средствами вы тоже не можете, это вопрос организации разработки и code review.
Разумеется, речь идёт об API клиентской библиотеки, а не сервера.
Обязать использовать именно его, конечно, нельзя — но вот, э-э-э, очень сильно рекомендовать можно.
Много ли вы видели программистов, которые полезут подключаться к порту СУБД напрямую при наличии официальной клиентской библиотеки? Да и при её отсутствии программист скорее сменит СУБД либо язык программирования чем будет городить велосипед.
Много ли вы видели программистов, которые полезут отправлять запросы через сырой SQL с конкатенацией данных вручную, если в проекте принято использовать ORM и именованные параметры?)
Достаточно видел таковых.
Подозреваю, что медианный вчерашний студент знает азы SQL (ибо они входят в курс по базам данных), но не слышал про ORM — я сам был таковым, недолго правда.
И сюда же надо добавить "динозавров", которые умеют динамически формировать запрос чтобы выполнить его через exec, но не любят ORM из-за "недостаточной гибкости" — этих я видел на Хабре.
А вот напрямую подключаться к порту не станет ни тот ни другой, первому скилов не хватит, второму лень будет.
Если сериализация написана неправильно, у вас все равно будут инъекции
Да что вы прицепились к сериализации-то? Ну то есть понятно, что эта сентЭнция, взятая сама по себе, возражений не вызывает. Успокойтесь уже, с этим никто не спорит.
Человеческий фактор при составлении запроса тут ни при чем.
А вот здесь вы облажались по полной. Причем, я уверен, не специально. Если бы вы не упарывались в эту абстрактную полемику про сериализацию, а немного вернулись в реальность, то первый бы со мной согласились. Что инъекции — это исключительно, 100% человеческий фактор. Все вопросы сериализации, что ручной, что автоматической, давно решены. Вопрос только в применении.
- Если человек применяет параметризованные запросы, чтобы АПИ/БэДэшечка сериализовывли данные за него — он молодец и пионерам пример.
- если человек собирает запрос из кусочков, самостоятельно сериализуя его элементы, придем делая это осмысленно и адекватно — он жыдай и мастер света, но ходит по краю
- если человек собирает запрос из кусочков, но из всех знаний у него "функция муэскуэль искейп стринг защищает от инъекций", то вот она, инъекция в полный рост. Не потому что какие-то мифические баги в реализации сериализации, а потому что кто-то просто не умеет ее готовить.
Инъекции — это 100% человеческий фактор.
Да что вы прицепились к сериализации-то?
Я уже несколько раз повторил — потому что инъекция возникает на этапе сериализации рантайм-значений в массив байтов, которые будут переданы по сети.
первый бы со мной согласились. Что инъекции — это исключительно, 100% человеческий фактор
Перекладывая сериализацию на машину, будь то привычные подготовленные выражения, или воображаемое дерево, мы как раз и избегаем инъекции
Нет. Еще раз, при использовании Query Builder для SQL никакого человеческого фактора нет, хотя запросы вы составляете сами. Механизм сериализации Query Builder будет правильно сериализовывать все ваши значения, хотя это не дерево AST.
Использование дерева в оперативной памяти не предотвратит инъекцию, если как тут предлагают "писать в сеть строки как есть". Я спорю именно с этой точкой зрения — "можно иметь AST в приложении и сериализовывать его как угодно, инъекции не будет". С AST нужно точно так же правильно продумать сериализацию, как и с SQL.
У универсального формата свои проблемы.
Представьте, что вы в своём запросе принимаете не просто строку, а тоже S-выражение. Ну, универсальный формат же, его и использовать надо универсально?
В таком случае пользователь просто подменяет в отправляемом браузером запросе (StrLiteral "Robert")
на (Subquery (Drop (Table users)))
и у вас снова проблемы.
Он не может так называться, т.к. в S-выражениях чётко отделены переменные и структура. Корректный парсер S-выражений делается легко и однозначно, а при генерации S-выражений в текстовом виде нужно лишь правильно и однозначно заэкранировать строковые переменные (это вам придётся сделать ровно один раз — в процедуре pretty-print
).
Если не типизировать/валидировать ваши выражения — ещё как может. Вот представьте на минуту, что типовой запрос на регистрацию на некотором сайте выглядит вот так:
(SignUp
(StrLiteral "Username")
(StrLiteral "email@example.com")
(StrLiteral "password")
)
Зачем тут вообще StrLiteral? Ну, допустим иначе парсер не поймёт как это в памяти представлять, потому что у языка типизация динамическая… Или вообще от фреймворка какого-нибудь осталось.
Ну при передаче это и так понятно, что это строка. Поэтому оно должно быть написано по-другому. И не очень понятно, откуда и куда вы это отсылаете.
Типовой запрос в SQL выглядит не совсем так. Он начинается либо с Update
, либо с Select
.
Проблема в том, что в вашем S-выражении мне вот, к примеру, тоже понятно что "Robert'); DROP TABLE users;"
— это строка. Но это не помешало вам зачем-то добавить StrLiteral. Возможно, вы как раз и пытались тем самым уйти от обсуждаемой проблемы, а возможно просто поставили по привычке.
Но это не важно, важно что у кого-то могут точно так же найтись причины добавить аналогичную конструкцию и в запрос, после чего при попытке срезать угол в очевидном месте будет беда.
Типовой запрос в SQL выглядит не совсем так. Он начинается либо с Update, либо с Select.
Хорошо, (Subquery (Update [(IsAdmin true)] (EqExpr (Var name) "Robert")))
вас устроит?
Но это не помешало вам зачем-то добавить StrLiteral.
Ну потому, что я тупо написал AST для запроса. Поскольку я писал на Хаскеле, то ассоциации насчёт типов всплыли сразу. Если взять грамматику для ANTLR4, то SELECT
там выглядит чуть по-другому — https://github.com/antlr/grammars-v4/blob/master/sql/sqlite/SQLiteParser.g4#L392
Вот expr оттуда — https://github.com/antlr/grammars-v4/blob/master/sql/sqlite/SQLiteParser.g4#L265
И literalValue может быть чем-то из
NUMERIC_LITERAL
| STRING_LITERAL
| BLOB_LITERAL
| NULL_
| TRUE_
| FALSE_
| CURRENT_TIME_
| CURRENT_DATE_
| CURRENT_TIMESTAMP_
Хорошо,
(Subquery (Update [(IsAdmin true)] (EqExpr (Var name) "Robert")))
вас устроит?
В общем да. Только вот это всё — не строка, а объект деревянного типа :-)
Ну не совсем. Это довольно урезанный функционал. Плейсхолдер может заменить только data literal — строковый или числовой. Все остальные части запроса идут как есть
Раздельная передача запроса и данных не обязательно использует prepared statements. Плейсхолдеры могут работать и без prepared statements. Например pg_query_params так работает.
pg_query_params — это просто обертка :)
Хелпер. Синтаксический сахар.
Но внутри у нее тот же самый подготовленный запрос :)
Нет, под капотом вызов PQexecParams. Подготовленный запрос при этом не используется.
Да прочитал я уже в документации. Да, формально PQexecParams — это не подготовленный запрос. Надо взять себе за правило использовать слово параметризованный, чтобы не становиться объектом таких придирок.
Я извиняюсь за обиженный тон в предыдущем комментарии. Вы совершенно правы, формулировки должны быть точными. Даже если с точки зрения конечного пользователя разницы и нет. Надо было с самого начала говорить про параметризованные запросы, чтобы разночтений не возникало.
Главное достоинство SQL, позволяющее ему быть на коне уже десятки лет, когда почти все современные ему языки уже канули в лету — это лаконичность и читабельность. которые такой отправкой будут полностью уничтожены.
Но некоторой альтернативой являются различные QueryBuilders
Заметьте, по какой-то причине никто до сих пор вашу распрекрасную теоретическую идею не реализовал. Даже LINQ, который тупо добавляет в C# операторы SQL, все равно в конечном итоге генерирует SQL запрос и уже его отправляет в базу. Что делает его обычным квери билдером. Вы так замечательно теоретизируете — наверняка ведь можете придумать красивое объяснение и этому факту?
Проблема конкретно SQL уже решена и так. Вот и ответ.
Вы тут все хотите, чтобы хоп, и случилось чудо. Чтобы что-то предложить новое и интересное, надо хорошо поработать, и не факт, что оно будет принято миром.
Понимаете, ну нельзя с такой дурацкой агрессией беседовать, обсуждая гипотезы. Это бессмысленно: очевидно, что гипотеза на то и высказывается, чтобы её дружелюбно и конструктивно критиковали. Будете нападать — ну я просто перестану вам отвечать, такой личный серый бан, и только. Ни больше, ни меньше.
Так вы же не обсуждаете. Вы выдаете свою гениальную идею, а как до дела доходит — "а я не хочу вдаваться в детали практики!"
А если вдаваться, то получится, что в API доступа к БД надо будет вынести неслабую такую часть логики СУБД, и при этом меняющуюся от версии к версии. И мало того, что при этом код простого транспортного АПИ критически раздуется, но он же будет должен постоянно меняться. Если сейчас API может использоваться лет 10 без изменения, то для передачи дерева придется держать зоопарк библиотек и менять версию при обращении к каждой БД.
Не говоря уже о том, что вы собственно задачу составления дерева перекладываете с компьютера на программиста. Вы не боитесь, что вас проклянут до седьмого колена все, кому придется писать вот этот многоэтажный код?
Заметьте, по какой-то причине никто до сих пор вашу распрекрасную теоретическую идею не реализовал.
А как же MongoDB?
В данный момент изучаю Express, по курсу вводят работу с express-validator. Там и валидация и escape() для тела входящего запроса. Это достаточная мера предосторожности для работы с экспресс сервером и нереляционной базой вроде монго? Для SQL базы нужно поверх этого еще sql валидатор использовать?
PS В изучаемом мной материале валидация данных и на стороне клиента и на сервере предполагается обязательной. Был удивлен, что в статье это представляется опцией.
Вы же читали статьи про правильную валидацию почты? Ну или хотя бы уверены, что используемая вами библиотека четко проверяет все варианты из RFC 6854 типа Pete(A nice \) chap) <pete(his account)@silly.test(his host)>
Поубивать бы таких валидаторов, которые не позволяют user+anything@host.com... По +anything потом прекрасно понятно от кого повалил спам. Но часто валидаторы в ступоре от плюса.
Иногда плюс запрещают специально, чтобы не было нескольких аккаунтов на один емейл.
Очень хороший вопрос.
Про escape()
Ну вот это как раз та самая глупость, о которой и говорится в статье. Это те грабли, по которым все РНР фреймворки уже прошли, но Express зачем-то решил наступить тоже.
В статье прямо говорится, что escape() для тела входящего запроса делать не следует. Именно потому, что ни к с экспресс серверу, ни к базе вроде монго, ни к SQL эта функция никакого отношения не имеет. Она нужна только при выводе данных в HTML контекст. А на вводе ей делать нечего.
Спасибо за развернутые ответы =)
Про искейп понял. До этого думал, что этот метод переводит спецсимволы в юникод, но перечитал статью на MDN и понял, что он их просто вытирает. Там же написано, что эти символы могут быть для cross-site scripting attacks использовано.
Про базы и необходимую доп санацию понятно объяснили, спасибо. Можно поподробней про плейсхолдеры для SQL запросов? Не очень понятно что имеется ввиду и как работает.
PS не уверен, что express-validator это часть экспресса, выглядит как сторонняя мидлвара - лежит на npm, доки на отдельном сайте.
Плейсхолдер, это когда SQL запрос не собирается из статичных частей и переменных, а является полностью статичным. При этом на месте подставляемых переменных в запросе стоят знаки вопроса. Т.е. вместо
'SELECT * FROM persons WHERE PersonId = ' + id
будет
'SELECT * FROM persons WHERE PersonId = ?'
а само значение переменной будет подставлено во время выполнения. А система уже дальше сама обработает переданные переменные как надо, чтобы они не нанесли вреда запросу.
Про валидацию.
В первую очередь надо понять, что статья не про валидацию вообще. Она она в первую очередь про безопасность. И про логику действий разработчика. А про валидацию — это просто рекомендация. Но, тем не менее, вопрос очень хороший, потому что люди часто путают валидацию с обеспечением безопасности.
Так вот, к безопасности валидация не имеет никакого отношения. И с этой точки зрения любая валидация опциональна. Хоть клиентская, хоть серверная.
Валидация — это часть бизнес-логики. И поэтому всегда разная. Это принципиально неформализуемое понятие. Невозможно заранее составить набор правил валидации для любых данных. То есть это такое достаточно неопределенное понятие, используемое для удобства. Это "хорошая" практика, а не обязательная. Безопасность приложения при отсутствии валидации не пострадает.
Говоря о том, что валидация на клиенте является опцией, автор исходит из логики. Из того простого факта, что клиентская валидация вообще ничего не гарантирует, поскольку ее легко обойти.
С точки зрения юзабилити — валидация на фронте скорее обязательна, да. Но статья вообще про это. Так понятнее?
Безопасность же, в отличие от валидации — обязательна.
И при обеспечении безопасности никакую валидацию использовать нельзя. У безопасности свои собственные правила, никак на валидацию не завязанные.
В частности, при выводе данных в HTML контекст в них необходимо экранировать управляющие символы HTML.
При составлении запроса SQL необходимо соблюдать два основных правила:
- Все данные должны попадать в запрос строго через плейсхолдеры
- любые другие элементы запроса, долбавляющиеся в него динамически, должны фильтроваться с использованием явно прописанного списка допустимых значений.
Реализация этих двух правил не обязательно должна быть явной, а может скрываться внутри какого-нибудь хелпера, но в конечном итоге составление запроса должно строго им следовать.
Теперь, я думаю, вы можете самостоятельно ответить на свой вопрос,
Для SQL базы нужно поверх этого еще sql валидатор использовать?
Не вижу никакого "упора" на символе &. Он упоминается только в одном примере, про неправильную обработку входящих данных. А статья вообще не про него.
Но вопрос интересный.
Он приводит нас к важному выводу: безопасность, по сути, вторична. Главная причина, по которой мы занимаемся всем этим экранированием — это чтобы код всегда был синтаксически корректным, вне зависимости от того, что мы в него напихали. А код, синтаксическая корректность которого не зависит от входящих в него данных, всегда будет безопасным. Просто в качестве побочного эффекта.
Подготовленный запрос с плейсхолдерами как раз является таким примером — какие данные в него не передай, на сам запрос они повлиять не смогут.
Теперь если вернуться к несчастному амперсанду. Уязвимость с его участием придумать действительно трудно. Но, тем не менее, в некоторых стандартах HTML он является зарезервированным символом (ссылка на стандарт, как просили). И, по идее, должен экранироваться. Да, большинство браузеров игнорируют незакодированный амперсанд в HTML. Но стандарт-то никто не отменял! По стандарту положено? Кодируем. Тем более, что в отличие от браузеров, большинство парсеров XML куда более привередливы, и тупо откажутся принимать документ, не соответствующий стандарту.
То есть это вопрос не уязвимости, а следования стандартам.
Ещё раз.
Пример совсем не про опасность. А про методы борьбы с ней.
Ваши претензии очень похожи на реакцию Буратино из известной книжки, который не может абстрагироваться от физической сущности яблок, и воспринять их только как условие в математической задаче:
— Предположим, что у вас в кармане два яблока. Некто взял у вас одно яблоко. Сколько у вас осталось яблок?
— Два.
— Подумайте хорошенько. Буратино сморщился, — так здорово подумал.
— Два…
— Почему?
— Я же не отдам некту яблоко, хоть он дерись!
Ну если вам так прямо не дает жизни этот амперсанд, давайте я специально для вас придумаю другой, не менее дурацкий пример:
Например, на NaiveSite ученики пишут математические примеры. Bob решает задачу, "Является ли верным условиеx > 0
приx = 1
?", но код фильтрации удаляет>
, и задача становится нерешаемой.
Теперь ваша картина мира становится целостной?
По поводу ваших проблем с мощностью сервера. Вы совершенно правы, эта частная проблема не совсем соответствует реальным требованиям к веб-приложениям.
При необходимости действительно можно придумать какое-нибудь кэширование уже отрендеренного содержимого при использовании маркдауна, или делать валидацию на входе, при использовании HTML форматирования. И затем при выводе явно отключать дефолтное форматирование для таких фрагментов данных.
Но, как я уже выше писал — это частный случай, а в статье приводятся общие принципы построения веб-приложений.
Что дешевле для мощностей сервера, один раз экранировать при написании комментария или каждый раз при выводе обрабатывать весь их набор?
А как конкретно вы будете их экранировать? Для вывода в HTML, в JavaScript, в URL, в SQL? Или для всего сразу? А если потом надо будет вывести в CSV, а у вас значение уже "Алиса&Боб"
вместо "Алиса&Боб"
? А через очередь сообщений в другую систему вы тоже будете отправлять "Алиса&Боб"
? А в той системе тоже следуют вашему подходу и экранируют при входе, и там уже получится "Алиса&amp;Боб"
.
Экранирование вывода, а не ввода — по большей части излишняя нагрузка на веб-приложение с точки зрения производительности.
Эта нагрузка совершенно незначительная по сравнению с сетевым вводом-выводом и работой с БД. PHP тоже дает дополнительную нагрузку по сравнению с ассемблером, и гораздо больше, чем экранирование строк, предлагаете сайты на ассемблере писать?
Правила экранирования символов зависят от синтаксиса языка, в который выводятся значения. На входе неизвестно, куда будут выводиться значения, и возможных вариантов тут больше одного, как минимум SQL и HTML.
Не надо подозревать. Вы не Шерлок Холмс. И даже не доктор Ватсон. Перед тем, как высказывать свое недовольство статьей, которую, после всех объяснений, вы так и не поняли, попробуйте почитать стандарт, на который я дал ссылку. Или хотя бы попробовать руками, сломается ли ссылка в атрибуте тега <a>
, если в ней заменить амперсанд, или нет.
амперсанд экранировать не нужно
На входе не нужно экранировать вообще ничего. А на выходе нужно экранировать всё, что требует разметка, куда выводится значение, в соответствии с ее правилами. Вы почему-то проинорировали аргумент про вывод в CSV и продолжаете спорить дальше.
он может быть в ссылке или как вы правильно написали, два раза быть экранированным.
2 раза экранированным может быть любой экранированный символ.
Чем больше количество пользователей, тем явнее сказывается экономия на каждом.
Нет. Потому что чем больше пользователей, тем и общая полезная нагрузка больше. И на фоне нее экранирование строк для этого количества пользователей остается незначительным. Если она для одного пользователя занимает 0.000001%, то и для 100000 пользователей будет занимать 0.000001%.
Послушать Вас, так вообще, если с PHP иметь дело, то можно смирится и руки сложить.
Если послушать меня, то не надо экономить на спичках. Тем более способами, которые создадут проблемы в другом месте. В вашем подходе вы будете или деэкранировать HTML обратно перед выводом в CSV, или хранить дополнительно в базе исходные данные, что и занимает место, и добавляет нагрузку на базу при выборках.
Экономить надо на том, что занимает значительные ресурсы.
Нужно ли отдавать неэкранированную XSS-уязвимость в третьи руки, вопрос уже морали и риска.
А кто предлагает отдавать неэкранированную? Данные в базе никакую XSS-уязвимость не содержат, она появляется только при выводе конкретно в HTML. При выводе тех же данных в CSV или в JSON XSS-уязвимости нет.
Кто то где-то забудет её на выходе зачистить
Чем это отличается от того, что кто-то забудет ее зачистить на входе?
Экранировав их на входе, вы будете более спокойны за безопасность, чем храня потенциальную уязвимость в БД.
С чего бы вдруг имя пользователя или описание в профиле это уязвимость?
Ваше дело хранить данные так, как ввел их пользователь. Будете коверкать мои данные, я буду пользоваться другим сервисом, где программисты знают, как предотвращать уязвимости.
А для CSV вы можете их преобразовать
Ну так и для HTML вы точно так же можете преобразовать, о чем и речь.
Я вам больше скажу, сейчас данные вообще зачастую не выводятся в HTML. Делается API, которое отдает их в JSON, а фронтенд-фреймворк вставляет их в ноды DOM-дерева.
На ввод же может попасть и уже экранированный контент, как быть в таком случае?
Хранить так, как ввел пользователь. Тут воообще нет никакой проблемы. Ввел имя с угловыми скобками, получил в своем профиле имя с угловыми скобками.
если поменять небольшое условие на главной google
Вот как раз для Гугла экранирование на входе создаст больше проблем, чем решит. Примеры я уже приводил, теперь масштабируйте их до размеров Гугла. Он сохраняет историю поиска для пары миллиардов пользователей, а вы предлагаете ему использовать в 2 раза больше места на диске, чтобы хранить в базе и экранированное значение и исходное.
СSV важный аргумент, но мы говорим про какие-то конкретные сайты?
Мы говорим про правильный подход, который позволяет создавать хорошо поддерживаемые приложения.
Правильный подход — экранировать в месте вывода. Потому что экранирование зависит от места вывода, и разных форматов в этих местах может быть больше одного.
Просто для большинства это не нужно.
Для большинства и экранирование при выводе в плане потребления ресурсов будет незаметно.
Чуть ли не через каждое сообщение пишу, что подход должен быть индивидуальным.
Нет. Логически есть единственно верный подход — выводить в соответствии с правилами вывода в этом месте.
В основном ввод данных встречается в меньшинстве мест, чем вывод.
Да, только в основном вывод встречается в разных форматах, а не только в HTML.
На моих сайтах эти три символа запрещены в именах пользователей, согласитесь, это не сильное ограничение.
Не соглашусь. Я как пользователь вижу, что вы во-первых коверкаете мои данные, во-вторых не умеете правильно предотвращать уязвимости, что вызывает сомнения в вашем профессионализме и качестве вашего сайта в целом. Если вы думаете, что предварительное экранирование HTML предотвращает например инъекции в SQL, то на вашем сайте вообще не стоит регистрироваться. Я лучше пойду на другой сайт, где в имени можно вводить угловые скобки, и они при выводе не ломают HTML-разметку. Это говорит о том, что там программисты умеют правильно обрабатывать входные данные.
Вы по API же отдадите экранированные скобки?
Нет, зачем мне для JSON экранировать скобки? Для JSON нужно экранировать кавычки, а не скобки. Функция json_encode делает это сама. Так же как htmlspecialchars делает это для скобок.
Вы бы стали выводить заэкранированную хоть ссылку на сайт с уязвимостью, а имя или что то там бы начиналось " Введи этот промокод..." и тд?
Извините, я не понял это предложение. Во фразе "Введи этот промокод" нет символов, которые надо экранировать для HTML, ее можно вывести и с вашим подходом.
Откуда вообще взялся сайт с уязвимостью? Я ничего про такие сайты не говорил. В моем варианте на сайте уязвимости нет, она предотвращается с помощью функции htmlspecialchars.
Здесь мы настолько разошлись в понимании, что лучше прикрыть эту тему.
Понятно, аргументов значит нет.
Вы предлагаете подход, который большинством программистов считается плохим и сложным в поддержке. Его можно применять только на каких-то мелких сайтах, где нет фронтенд-приложений, ajax-запросов, данные вводятся и выводятся только через веб-интерфейс, и их можно изменять как хочется программисту. И то только потому что в силу небольшого размера на них незаметно, что он плохой и сложный в поддержке.
Напротив, утверждаю, что нет одного подхода для множества вариантов решений.
Ну вот в том и дело, что вы не понимаете, почему надо экранировать именно так. Это не просто чье-то мнение, у этого есть логическое обоснование.
Вот вы написали "хорошо поддерживаемые приложения" — шаблонная фраза, не подкреплённая аргументацией.
Специально для вас скопирую аргументацию еще раз.
"Потому что экранирование зависит от места вывода, и разных форматов в этих местах может быть больше одного."
"Тем более способами, которые создадут проблемы в другом месте. В вашем подходе вы будете ..."
"Поддерживаемость" это про то, что если завтра придет менеджер и попросить сделать выгрузку в CSV, вам не пришлось бы переписывать много кода или писать странные обрабатывающие конструкции, причины которых непонятны для другого программиста. Также см. пример далее.
И вообще вот этот "правильный", он через пару лет будет такой-же правильный?
Да. Потому что логическое обоснование никуда не исчезло. Правила экранирования зависят от выходного контекста. При выводе в разные форматы надо по-разному экранировать. Из чего логически следует, что экранировать на входе принципиально невозможно. За исключением простого вырожденного случая, когда ввод всегда через HTML и вывод всегда в HTML.
Может перед внедрением все таки подумать, в рамках какого приложения и что будет нужно.
Я привел пример именно про то, чтобы подумать. Понадобится выводить в JSON? С большой вероятностью да. Понадобится CSV? В ближайшее время нет, потом скорее всего да, менеджерам нужны выгрузки данных.
Но написал её продолжая свою мысль про три символа предлагаемых для экранирования
Какая разница, 3 или не 3, htmlspecialchars всё равно экранирует все.
решит большую часть проблем с XSS
Экранирование на выходе решит все проблемы с XSS. Равно как и с любыми другими инъекциями.
Зачем вообще использовать средство, которое решает часть проблем, если можно решить все?
все остальные возможные символы нужно преобразовывать при выводе в этом формате. С этим я не спорю
Тут проблема не в том, что надо экранировать в тех форматах, а в том, что у вас уже в базе записано "Алиса&Боб", и оно в таком виде попадет в любые другие форматы.
Логически есть единственно верный подход — выводить в соответствии с правилами вывода в этом месте.
Круто, твердая позиция. Но у меня другая.
Если вы будете выводить не в соответствии с правилами вывода для некоторого формата, у вас будут возможны инъекции кода. Это не чья-то позиция, просто так работают компьютеры.
Количественно это html, напомню, имею ввиду экранирование только для него. Это же веб, ё-моё.
Большое количество HTML отменяет необходимость экранировать по-другому для других форматов (для чего придется предварительно деэкранировать HTML)? Или как это возражение связано с тем, о чем я сказал?
Был же пример про стандарную отдачу контента с бекенда на фронт по json, а потом если в HTML вставляется, то любой валидный тег будет интерпретирован как тег.
Нет.
var data = JSON.parse('{"name": "<div>A&B</div>"}');
var input = document.createElement('input')
input.value = data.name
document.body.appendChild(input)
var div = document.createElement('div')
div.appendChild(document.createTextNode(data.name))
document.body.appendChild(div)
В этом коде данные интерпретируются как текст. Я говорил про фронтенд-фреймворки, они не вставляют данные как HTML-разметку, они манипулируют элементами DOM. А если где-то и вставляют, экранируют сами, поэтому вы должны отдавать неэкранированный текст.
К слову, в этом случае экранирование переносится на клиент и не потребляет ресурсы на сервере.
призыв к другим пользователям скопировать и запустить скрипт или ещё что-то в этом роде, от имени администрации, например, хоть это и заэкранировано
И как кодирование угловых скобок на входе предотвратит вывод просьбы с javascript-кодом, в котором их может вообще не быть?
Каждый раз, каждую секунду, с каждым запросом. Зачем?
Я уже несколько раз написал зачем. Чтобы не декодировать html перед выводом в другие форматы, чтобы не хранить рядом поле с исходными данными, чтобы не было возможности двойного экранирования при передаче между системами или частями кода, чтобы не было проблем с подсчетом длины и поиском по LIKE.
Что касается CSV, то скорее всего вам не представит труда преобразовать спецсимволы обратно в символы.
Ну так я и говорю, выбор или преобразовывать спецсимволы при выводе в HTML, либо де-преобразовывать специсимволы при выводе в CSV/JSON/TXT. Нет такого, что преобразовали один раз на входе, и потом больше преобразовывать не надо.
То есть сначала — это правило одно для всех, а вот сейчас оказывается, что и мелкие сайты есть
Да я вроде сказал, что и для мелких сайтов тоже надо использовать правило "экранировать при выводе". Так что ничего там особенного не "оказывается".
И что не так с ajax-запросами?
Я уже несколько раз написал, в вашем варианте в ajax-запросе придет ответ в формате JSON, в котором будет строка "Алиса&Боб", и она будет так выведена в интерфейсе или в текстовом поле. В JSON надо отправлять исходные данные, без HTML-кодирования.
Представьте, что у вас новостной сайт, а пользователь впихнул угловые скобки себе в имя, и вот он везде маячит, пишет комменты, а вы его имя конвертируете и конвертируете серверными средствами для каждого показа страницы
Ну я и говорю, для мелких сайтов нормально, где кроме HTML ничего нет. Не "правильно", а "нормально", то есть негативные стороны такого подхода незаметны.
А потом приходит менеджер и говорит "А почему количество символов в статье и комментарии неправильно считается, в тексте написано "A&B", а показывает длину 7? И в поиске по этой фразе не ищет.". Это к вопросу о поддерживаемости.
Иногда это оправдано для больших полей, например для исходника в Markdown можно хранить рядом преобразованую копию в safe HTML, чтобы не преобразовывать постоянно при выводе, тут экономия ресурсов получается заметная. Но это не подход по умолчанию, как вы утверждаете. На сайтах типа Хабра, которые вы привели в пример, предварительно HTML-экранированные комментарии может и есть, но хранятся вместе с исходным текстом, а не вместо него. И часто не в базе, а в кеше. Это не санитизация на вводе, а просто кеширование.
подход, который используется только там у вас в конкретном размере сайта
Это не только у нас. Везде, где я работал, экранирование данных было при выводе. Такой подход используется например в Twig, а Twig это часть Symfony, и его использует много людей.
"Automatic output escaping: To be on the safe side, you can enable automatic output escaping globally or for a block of code"
not-an-email – валидный по формату адрес электронной почты. Он принадлежит пользователю с таким именем на текущем сервере.
Поубивал бы людей, валидирующих e-mail'ы по формальным признакам.
эээ? А не могли бы вы донести свою мысль чуть более развернуто?
В частности, какое отношение ваш комментарий имеет к статье про экранирование, и каким образом текст not-an-email является "валидным по формату адресом электронной почты".
В частности, какое отношение ваш комментарий имеет к статье про экранирование
В статье приведена возмутительная иллюстрация:

и каким образом текст not-an-email является "валидным по формату адресом электронной почты".
Выкиньте свой PHP на помойку.
На адрес not-an-email любой вменяемый почтовый клиент и сервер может послать электронную почту. Это то же самое, что not-an-email@доменмоегоsmtpсервера.
А, так вы про иллюстрацию.
Спасибо, теперь понятно.
… что-то рано весна началась в этом году
На адрес not-an-email любой вменяемый почтовый клиент и сервер может послать электронную почту.
Gmail и Outlook не отправляют. Их тоже надо выкинуть? Приведите пожалуйста пример "вменяемого" почтового клиента.
Apple Mail, например, отправляет.
Не имею аутлука, но что-то я сомневаюсь, что он не может отправить, так как он и к smtp-то, собственно, не привязан.
Outlook показывает сообщение "The address… is not formatted correctly. Please correct and try again".
Apple Mail показывает сообщение "… does not appear to be a valid email address. Verify the address and try again." с кнопкой "Send Anyway". То есть даже Apple Mail не считает это валидным адресом.
Это то же самое, что not-an-email@доменмоегоsmtpсервера.
Откуда сайт должен знать, какой там у вас "вашsmtpсервер", если на свой домен он почту не принимает?
Сервер smtp имеет свой основной почтовый домен (в постфиксе это $mydomain). Он и используется по умолчанию. Как у вас настроены ограничения приёма почты – ваша забота.
Изначально в передаче почты вообще не предусматривались разные хосты (и, как следствие, разные почтовые домены), а почта передавалась между пользователями одного компьютера, поэтому часть с @ – это расширение почтового адреса.
Еще раз, на какой домен сайт "mycoolcompany.com" должен отправлять письмо о регистрации, если вы ввели "not-an-email" и не являетесь сотрудником этой компании?
Это не дело сайта – вообще об этом думать. Он должен отправить письмо на тот e-mail, который я ввёл. Если в адресе нет домена, значит так надо.
Я тут вижу, однако, какие-то неоправданные конкретизации общего вопроса. Я, например, испытываю неудобства, когда не могу ввести адрес без @ в поле рассылки алертов конфигурации собственного маршрутизатора, а не в какой-то там mycoolcompany.com.
В случае технической страницы в какой-нибудь админке или вовсе веб-интерфейса для инструмента местного применения (того же роутера) я с вами соглашусь.
Но вот для публичного сайта работает прямо противоположное — не дело пользователю знать какой там вообще основной домен у обслуживающего сайт SMTP сервера.
Это не дело сайта – вообще об этом думать.
Это именно что дело сайта, так как он должен отправить email. Куда он будет его отправлять?
Если в адресе нет домена, значит так надо.
Кому надо, вам? А администрации сайта не надо отправлять письма пользователям на свой собственный почтовый домен, так как пользователей на нём по их бизнес-сценарию в принципе быть не может.
Я тут вижу, однако, какие-то неоправданные конкретизации общего вопроса.
Конкретизация задана темой статьи и примерами в ней.
конфигурации собственного маршрутизатора, а не в какой-то там mycoolcompany.com
При этом почему-то приводите в качестве неправильного примера скриншот с полем "Wedding date", который явно далёк от маршрутизаторов, и в контексте которого валидация наличия домена оправдана.
Кому надо, вам? А администрации сайта не надо отправлять письма пользователям на свой собственный почтовый домен, так как пользователей на нём по их бизнес-сценарию в принципе быть не может.
Пользователю ничто не мешает ввести "not-an-email@mycoolcompany.com" с тем же результатом для администрации.
Ну и пусть вводит, это соответствует правилам валидации. Дальше уже работает подтверждение email. Непонятно, зачем разрешать ввод для заведомо неправильного формата, как вы предлагаете.
Это не заведомо неправильный формат. Это формат, соответствующий стандарту и имеющий определённую практику употребления.
Идя в русле ваших рассуждений, многие, например, считают, что русских букв не может быть в адресе (или вообще в любом тексте). Или косой черты в имени пользователя. И т.д.
Это заведомо неправильный формат для бизнеса.
потому что их авторы не до конца поняли требования к формату
Это вы не до конца поняли требования бизнеса. У бизнеса могут быть свои цели помимо следования формату.
А, теперь понятна причина вашей истерики.
Жаль только, что вы выбрали для нее совершенно неподходящее место.
Скажу по секрету, логические доводы здесь приводить бессмысленно :)
Со своей точки зрения человек, в общем-то прав.
Другое дело что его точка зрения не имеет никакого отношения к обсуждаемому вопросу. Но кого это когда смущало :)
К обсуждаемому вопросу это имеет самое непосредственное отношение: автор статьи сначала пишет много правильных слов про ненужный безопасный ввод, а потом сам же себе противоречит своим примером о валидации ввода.
Самая раздражающая вещь – это формы ввода, которые не дают ввести нужное для обработки значение, потому что их авторы не до конца поняли требования к формату.
Честно говоря, если судить по вашим публикациям и другим комментариям, вы не производите впечатление неадекватного человека. Это тем больше затрудняет атрибуцию вашей навязчивой идеи в данном случае. Ну то есть я догадываюсь, что у многих узких специалистов с возрастом развивается совсем уж туннельное зрение, и они принципиально не в состоянии понять контекст, а видят ровно два слова из всего текста и реагируют только на них. Но как-то в данном случае это принимает совсем уж гротескные формы.
Я не буду взывать к логике, и апеллировать к тому что адрес вводится в форму регистрации на веб-сервере, а не в форму отправки письма на SMTP сервер.
И я даже не буду просить вас вас привести реальный пример формы регистрации на веб-сайте, где ввод вашего бездоменного адреса будет осмысленным. Или же невозможность ввести такой адрес будет представлять из себя проблему.
Мне интересно другое. Если вы такой знаток стандартов, можете объяснить, как ваши слова соотносятся с rfc 822? Сразу признаю, я невеликий специалист. Но вот я вижу
6. ADDRESS SPECIFICATION
6.1. SYNTAX
address = mailbox ; one addressee
mailbox = addr-spec ; simple address
addr-spec = local-part "@" domain ; global address
domain = sub-domain *("." sub-domain)
sub-domain = domain-ref / domain-literal
domain-ref = atom ; symbolic reference
где здесь указание на то, что domain можно опустить?
Из контекста никак не следует, что это форма регистрации. Приведена просто некая веб-форма ввода почтового адреса, без уточнения, для какой цели.
Что касается стандартов. RFC-822 (даже и так нося рекомендательный характер) описывает формат почтового сообщения, чего мы вообще никоим образом не касаемся в нашем примере. Нас интересует адрес, на который отправляется письмо. Этот адрес задаётся командой RCPT протокола SMTP, описанного в RFC-2821:
Syntax: "RCPT TO:" ("<Postmaster@" domain ">" / "<Postmaster>" / Forward-Path)[SP Rcpt-parameters] CRLF
https://www.rfc-editor.org/rfc/rfc2821#section-3.6
The reserved mailbox name "postmaster" may be used in a RCPT
command without domain qualification (see section 4.1.1.3) and
MUST be accepted if so used.
Syntax:
"RCPT TO:" ("<Postmaster@" domain ">" / "<Postmaster>" / Forward-Path)
[SP Rcpt-parameters] CRLF
https://www.rfc-editor.org/rfc/rfc2821#section-4.1.2
Forward-path = Path
Path = "<" [ A-d-l ":" ] Mailbox ">"
Mailbox = Local-part "@" Domain
"postmaster" это не произвольное имя пользователя, это прямо конкретное зарезервированное имя в виде слова "postmaster". Если там указаны какие-то другие слова, это отновится к правилу "Forward-Path", в котором должен быть указан Domain.
Ну вы ещё скажите, что проверяете отдельно слово postmaster.
Практика такова, что проходит всё.
Где проходит? Вы начали эту ветку с жалобы на то, что не проходит. В стандарте это не разрешено, оно не проходит в клиенте от Google, от Microsoft, и даже Apple ясно говорит, что это неправильный формат.
Email без символа "@" не соответствует стандарту, поэтому общепринятая валидация на сайтах работает корректно. Оскорбления людей на основе ваших фантазий оставьте при себе.
Я начал ветку с того, что проходит через общеупотребительную почту, но не вводится в некоторые формы.
А клиент Apple и на пустой Subject выдаёт предупреждение – надеюсь, вы не станете клеймить и его?
Не волнуйтесь так.
надеюсь, вы не станете клеймить и его?
Его?) Я "клеймлю" ваши претензии. Если вы будете мне предлагать разрешать регистрацию с пустым email, то тоже буду клеймить. Apple все делает правильно — адрес невалиден, и он об этом сообщает.
Я начал ветку с того, что проходит через общеупотребительную почту
Не проходит. Ни Google, ни Microsoft такой адрес не разрешают.
Проверил в клиенте Apple Mail для почты Google, приходит ответ "The recipient address… is not a valid RFC-5321 address".
То что в Apple работают криворукие программисты, которые в своей почте неправильно реализовали стандарт, не означает, что так должны делать все остальные.
Итого, ваше исходное утвержение "not-an-email – валидный по формату адрес электронной почты" неверно, дальнейшее обсуждение не имеет смысла.
К сожалению, из вашего комментария следует, что предметной областью не владеете именно вы.
Давайте с вами немножечко подумаем. Вот вы предлагаете:
Есть функции преобразования специальных символов в html сущности и обратно. При сохранении в бд преобразовал, при выводе преобразовал обратно. Самый простой вариант и думать не над чем.
Соглашусь — вариант простой. Вот только непонятно, а в чем, собственно, смысл этих телодвижений? На выходе мы получаем тот же самый текст. Зачем его было туда-сюда перекодировать?
Или вот
Я уверен, что ни один владелец сайта не собирается собирать базу неактивных пользователей типа Robert'); DROP TABLE users;. Поэтому такой бред и вычищается на этапе ввода.
А можете привести пример этой самой валидации? Я уверен, вы же ее постоянно применяете, правда же?
Давайте пока оставим автора, и вернемся к вашему предложению, которое я процитировал в предыдущем комментарии, вот этому:
Есть функции преобразования специальных символов в html сущности и обратно. При сохранении в бд преобразовал, при выводе преобразовал обратно. Самый простой вариант и думать не над чем.
Обещаю, как только вы дадите мне ответ на вопрос — в чем смысл этих телодвижений? — я сразу же начну обсуждать с вами автора, и то, на чем он акцентирует свое внимание. Договорились?
На скорую руку набил regex
Скажите, я правильно понимаю, что "на скорую руку" вам пришлось набивать потому, сами вы не относитесь к тем самым владельцам сайтов, о которых писали выше? Ну, просто если бы относились, то скопировали бы из своего кода — верно?
^[a-zA-z0-9_]*$
Занятно. А что если пользователя зовут Д'Артаньян? Ну или скажем Шакил О'Нил? Отказать ему в регистрации?
Но, главное — что делать, если кроме логина нам нужна еще какая-то информация? Ну, скажем, возможность оставлять комментарии? Для этого, весьма редкого, случая вас не затруднит набить regex? Ведь если мы защищаемся от инъекций валидацией, как вы предлагаете, то она должна быть для любых данных, а не только для одного дебила логина? ;)
del
Не пытайтесь обезопасить ввод. Экранируйте вывод