Как стать автором
Обновить

Комментарии 58

Крутая статья, спасибо!

Заметили, что будет PY PI версия? А следющая будет PEPE /sarcasm_off

В честь версии вполне можно было бы в шутку зарелизить Pithon 3.14.

Ждём jinja3!

Я привередлив. Это кому-то надо, кому-то нет. Значит, должно быть вне языка и импортироваться. Хотя бы потому, что медленнее.

С другой стороны, есть f-строки и всё выглядит красиво и нет никаких причин почему бы ни быть и t-cтрокам, и d-строкам, и q-строкам, и r-строкам... Значит, должно быть на уровне языка. Точно так, как и сделано.

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

r-строки уже давно есть, кстати. Наверное чуть ли не с первой версии.

Более того, на discuss.python.org недавно предложили d-строки, но это вообще уже, как по мне, шиза. Суть в том, что они автоматом будут убирать кучу отступов в начале строки при, например, таком использовании:

def f():
    print(
    """
        Hello, World!
        This is a second line
    """

Как по мне, так это либо надо изначально было делать (как поступили, например, в Java), либо сейчас уже просто жить с textwrap.dedent и не плодить всякое в язык.

С какой-то стороны с вами согласен, но это уже скорее философия конкретного языка.

Сейчас точно не вспомню, в дискассе под каким пепом это читал, но там был комментарий от Core Developer в духе:

Необязательно, чтобы то, что добавляет PEP, было полезно всем. Python использует большое количество людей, эта фича будет полезной определенной части комьюнити.

А насчет пакетов на добавление строк на любые буквы: у нас и так есть PEP263 и порт f-строк на Python 2 с помощью него, хотя это скорее грязный хак, чем задуманная фича.

Я понимаю что статья не о том, но

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

Не надо вводить людей в заблуждение. Все нормальные движки БД давно поддерживают параметры. Поэтому значения в параметризированный запрос не подставляются никогда. Опять же, парсеру запросов проще один раз распарсить, оптимизировать и закешировать `select * from users where name = $1` чем парсить тысячи разных запросов, которые отличаются только параметром. Я уж не говорю о том что это исключает SQL injection полностью и навсегда.

Да, вы правы, я не совсем правильно выразился. Исправил, чтобы не продолжать вводить в заблуждение. Спасибо!

Обычно DB разрешают использовать параметры лишь вместо значений. Но довольно часто приходится параметризовывать части самого запроса: названия схем, таблиц, полей и т.п. Здесь серверная интерпретация не работает.

Ну если вы параметризируете эти части кусками из user input, то у меня для вас плохие новости...

Шутка понятна, но здесь на самом деле больше нюансов в каждом диалекте SQL.

Ну почему же. Если экранировать то все хорошо. Если вы говорите про антипаттерн, то я напомню, что like '%text%' не так просто записать параметром

like '%text%' не так просто записать параметром

Смотря где, в оракле можно без проблем сделать like '%'||:P_SUBSTR||'%'

Если экранировать то все хорошо.

Если экранировать, то всё плохо. Очень плохо. Само по себе "экранирование" не защищает вообще ни от чего.

я напомню, что like '%text%' не так просто записать параметром

С каких пор запись обычного строкового значения перестала быть элементарной операцией, не говоря уже о каком-то "не так просто"?

Спасибо за статью.Мне уже страшно от обновлений python. Кажется что скоро будет легче писать на +сах чем на Пайтон🤔

неее, плюсы тоже развиваются и пока ещё вне конкуренции ;)

эхх, пойду в проституты тогда

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

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

а так я бросил писать на пайтон, ушел головой в CS и математику. и ваще пофиг на собесы и на работу. после статей на хабре я ваще потерял надежду. никогда бы не подумал что после столького труда, тяжелой учебы, стресса, депрессий, синдромов самозванца и прочих надо еще клянчить и попрощайничать чтобы взяли на работу. я такое только у врачей видел (жена врач, 10 лет учебы без личной жизни, скачет давление, здоровье село, платят меньше чем курьеру, а работает тупо ради опыта). Я лучше пойду в opensourse, убивать бизнесс корпораций зла и учавствовать в проектах что делают мир лучше. ну я пока учусь и представляю себе мир айти только таким. а что в реальности не знаю, как говориться делаю что нравиться и будь что будет!

И int и str форматируешь с помощью %s ? Фантазёр. Профи. Тэкнолоджиа.

Да, а что в этом такого? До тех пор, пока меня не интересуют специфичные для числа форматы (sign, zero padding) можно и %s использовать

Не совсем ясно, насколько такой подход лучше уже существующего

В таком виде это вообще не вариант.

Этот пример слишком упрощён, и в таком виде представляет опасность. Не существует такой волшебной функции, которая абсолютно любое передаваемое в запрос значение каким-то волшебным образом сделает безопасным от инъекций. Это миф, популярность которого может соперничать только с его вредоносностью. Берём элементарный запрос

t"SELECT * FROM users ORDER BY {sort_column}"

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

Чтобы использовать t-строки для защиты от инъекций, надо вместе с переменной передавать контекст - тип SQL литерала, на место которого передаётся переменная. И тогда парсер сможет корректно этот литерал отформатировать. И очень важно, чтобы вокруг литерала не было кавычек. Потому что строковый литерал включает и обрамляющие его кавычки тоже. И тогда форматтер либо сделает строковое экранирование и заключит значение в кавычки, либо тупо заменит его целиком на плейсхолдер, а значение в несёт в список переменных на execute().

И в таком виде потенциал у подхода есть. Но реализовывать его надо очень аккуратно, не с кондачка.

Не существует такой волшебной функции, которая абсолютно любое передаваемое в запрос значение каким-то волшебным образом сделает безопасным от инъекций

cmd.AddParameter()

И она нормально отработает запрос из моего комментария? Можно попросить привести полный пример?

SELECT * FROM 
...
ORDER BY case when <your_value> = '96005' then 1
          when <your_value> = '5806' then 2
          when <your_value> = '96004' then 3
          when <your_value> = 'PROD91' then 4
          when <your_value> = 'PROD187' then 5

Не, это так не работает - или это какой-то нетипичный диалект SQL. Это выражение для каждой строки выборки сформирует значение, и по нему будет сортировать. Но так как оно зависит только от параметра, то для всех строк получится одинаковым и сортировки де-факто не будет. Надо что-то вроде:

select id, val
from test
order by case <your_value> when 1 then id when 2 then val end

И в выражении надо приводить и id и val к одному типу.

Ага, и так для каждого значения в таблице. Прекрасная идея.

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

Нет, не забыл. Я даже процитировал вашу фразу, с которой хотел поспорить. Кстати, ниже есть комментарий, который объясняет один нюанс, о котором я забыл: колонка ордербай меняет запрос, а не параметризирует его. Мой вариант - как раз параметризация, соответственно все ветки выполнения должны быть а запросе. Это не сложно, если на момент выполнения запроса список колонок известен. Я бы тупо сверил эту колонку со списком колонок таблицы и склеил в строку без всяких параметров.

Так что давайте вернёмся к передаче значения, а не части запроса.

Изменение sort_column это уже изменение запроса, а не его параметризация. Не знаю ни одной библиотеки доступа к БД с "сырым" sql, которая работала бы на уровне DML. Для такой задачи нужен ORM или SqlBuilder.

Видимо, проблема в том, что о параметризации речь вообще не шла, а в статье говорится про экранирование?

Это кто как прочитал :) В контексте запроса sql и драйвера БД экранирование это всегда про параметры (bind переменные, плейсхолдеры).

Мда, с одной стороны, конечно отрадно, что выросло поколение, для которого существует только один способ поместить переменную в запрос :). Но почему вы называете параметризацию таким странным словом - "экранирование"? Что и зачем там экранируется?

Вопрос, разумеется, риторический. Всё-таки, исходно речь был про пример, приведённый в статье, и там очевидно речь идет именно об экранировании, а не подстановках. Хотя бы по той причине, что в запросе вокруг переменной стоят кавычки.

"Используем bind переменные с 2001 года" (tm) :) Поэтому очень странно видеть, что вы хотите еще и автоматическое экранирование идентификаторов. Если такое реализовать на t-строках, то это уже будет не sql для вашей БД, а отдельный dsl.

Хотя бы по той причине, что в запросе вокруг переменной стоят кавычки.

В статье пример для f-строк. Для t-строк кавычки должны быть убраны. Если это не так, то тоже хотелось бы увидеть комментарий автора :)

Пожалуй, вы правы, кавычки там действительно лишние)

А ещё я нашел то, как т-строки начинают внедрять в psycopg3: https://github.com/psycopg/psycopg/pull/1054, и там таки тоже без кавычек)

psycopg3

У него оказывается и поддержка идентификаторов есть) Но как я понимаю, это нестандартная вещь для Python Database API.

Там исторически подстановка параметров была на стороне клиента. Поэтому параметры можно было вставлять в любую часть запроса - в базу SQL запрос уходил уже с подставленными значениями. В третьей версии это переиграли и теперь по умолчанию используется server-side параметризация. Но обратная совместимость оказалась подломанной.

Но реализовывать его надо очень аккуратно, не с кондачка.

Абсолютно согласен. Собственно и ждем, когда умные дяди придумают как красиво и правильно все это применить. А тут пока просто примеры, что вообще можно в теории с ними сообразить. Ни один из приведенных в статье примеров не является production-ready, да, собственно, на это и не претендует

Берём элементарный запрос t"SELECT * FROM users ORDER BY {sort_column}" "экранируем" его с помощью воображаемой функции "sanitize" и получаем идеальную инъекцию, из палаты мер и весов.

Неправда, на выходе получится нерабочий запрос. Нерабочий запрос остаётся безопасным :-)

С какой это стати он будет нерабочим?

Мне кажется, тут никто либо статью не читал, либо вкладывает в слово "экранирование" какой-то свой особый смысл.

В статье приведён пример запроса

SELECT * FROM users WHERE username = '{username}'

В котором при вызове функции sanitize_sql() значение переменной username будет автоматически экранировано. Вы можете объяснить, с какой стати это экранирование сделает нерабочим запрос SELECT * FROM users ORDER BY {sort_column}?

В статье ошибка, библиотеки которые я видел используют запросы вида SELECT * FROM users WHERE username = {username}, соответственно возможности передать имя колонки параметром даже не предполагается.

Об этом и речь. Я-то комментировал не библиотеки, а статью. И писал про пример, приведённый в статье. Где используется совсем другой запрос, вида SELECT * FROM users WHERE username = '{username}' и в качестве защиты предлагается пресловутое "экранирование".

С одной стороны, радует, что комментаторы дружно не видят других вариантов, кроме подстановок. Но с другой - огорчает, что авторы статей всё ещё считают, что защитой от инъекций служит какое-то волшебное экранирование. У статьи влияние больше :(

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

Кстати, вот такой запрос можно заставить работать безопасно и ровно так как задумывалось:

t"SELECT * FROM users ORDER BY {sort_column:ident}"

Ну кстати, по крайней мере если судить по вопросам на на qna сайтах, из проблем экранирования я бы на первое место поставил наивную веру в то, что оно экранирует некие "опасные символы SQL" и поэтому защищает любой помещаемый в запрос элемент, на второе - сознательный отказ от защиты "эти данные и так безопасны!" (впрочем, это является проблемой и для подставновок), и только на третье - "забыл".

А насчет подстановки типов я совершенно согласен, это самый удобный и наглядный вариант. По умолчанию, ({username}), делается стандартная подстановка, а если указывается ещё и тип, ({sort_column:ident}), то обработка в соответствии с этим типом.

Хотелось бы уточнить:

Плюс на каждую операцию конкатенации создается новый объект строки, что бьёт по производительности

а все другие способы от этого не страдают?

Ну вот, например, сравнение с %:

PS C:\Users\Tapeline> python -m timeit "a=1; b='hello'; 'text %s text %s' % (a, b)"
1000000 loops, best of 5: 246 nsec per loop
PS C:\Users\Tapeline> python -m timeit "a=1; b='hello'; 'text '+str(a)+' text '+b"
1000000 loops, best of 5: 263 nsec per loop

На небольших размерах не существенно, но тем не менее, просадка есть

7 наносекунд - это что-то на уровне погрешности.
Но имел ввиду, что проблема генерации кучу стрингов она везде, даже в новомодном

    string = []
    for part in template:
        ...
        string.append(value)
    return "".join(string)

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

Эта фича полезна исключительно синтаксисом, а синтаксис невозможно "оформить" в отдельной библиотеке (по крайней мере, на Питоне)

T-строки позволяют сделать структурный логгинг фактически бесплатным

Ага, и получить накладные расходы на формирование шаблонов на каждое сообщение логгера, они же не ленивые! Лучше бы в logging добавили форматирование через {}. такое же как сейчас ленивое с аргументами, но с современным языком форматирования. Как в loguru. Сейчас все лепят logger.debug(f'Bla bla bla {super_heavy_object_to_format}') вместо ленивого форматирования, а теперь ещё и t-strings лепить начнут.

Структурированное логгирование делается просто через JSON-форматтер (python-json-logger) и фильтры/extra, благо стандартный логгер очень гибко конфигурируется. А structlog/loguru ни с чём не совместимые библиотеки.

logger.debug(f'Bla bla bla {super_heavy_object_to_format}')

Справедливости ради, они частично ленивые. Выражения считаются сразу, а вот форматирование откладывается.

Вы про t-strings?
Я про f-strings, что их пихают в логгер. И там ничего не откладывается. Всё форматируется в момент создания f-строки.

Простой пример для проверки:

import time

class A:
    def __str__(self):
        time.sleep(2)
        return 'hello'
        
def log(s):
    pass
    
a = A()

%time log(f'hello {a}')
CPU times: total: 0 ns
Wall time: 2 s

В итоге код тормозит даже с выключенными логами из-за бесполезного создания f-строк.

Так вы же пишете

«T-строки позволяют сделать структурный логгинг фактически бесплатным»

Ага, и получить накладные расходы на формирование шаблонов на каждое сообщение логгера, они же не ленивые!

Я и отмечаю, что т-строки частично ленивые, если так можно выразиться

У f- строк есть фичи, о которых мало кто помнит/знает. Тут можно глянуть, чтобы понять о чём я. Обычно используют в формате f'Hello, {username}', максимум для дебага/логов f'{my_var=}', и всё. А почему? Да потому что это просто и понятно. А для исключения всяких инъекций в БД используют инструменты...(внезапно!) работы с БД.
T-строки, видимо на порядок сложнее фич f-строк, о которых многие даже не вспоминают. И что-то мне подсказывает, что иметь они (t-строки) будут очень ограниченное применение.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации