Как я могу избежать полей в F-строку? - PullRequest
7 голосов
/ 11 июня 2019

Версия f-строк в Javascript позволяет выполнять экранирование строк с помощью довольно забавного API, например,

function escape(str) {
    var div = document.createElement('div');
    div.appendChild(document.createTextNode(str));
    return div.innerHTML;
}
function escapes(template, ...expressions) {
  return template.reduce((accumulator, part, i) => {
    return accumulator + escape(expressions[i - 1]) + part
  })
}

var name = "Bobby <img src=x onerr=alert(1)></img> Arson"
element.innerHTML = escapes`Hi, ${name}` # "Hi, Bobby &lt;img src=x onerr=alert(1)&gt;&lt;/img&gt; Arson"

. Позволяет ли Python-строки использовать аналогичный механизм?или вам нужно привезти с собой string.Formatter?Будет ли более питонная реализация переносить результаты в класс с переопределенным методом __str__() перед интерполяцией?

Ответы [ 2 ]

9 голосов
/ 15 июня 2019

Когда вы имеете дело с текстом, который будет интерпретироваться как код (например, текст, который браузер будет анализировать как HTML, или текст, который база данных выполняет как SQL), вы не хотите решать проблемы безопасности путем реализации Ваш собственный механизм побега. Вы хотите использовать стандартные, широко протестированные инструменты для их предотвращения. Это дает вам гораздо большую безопасность от атак по нескольким причинам:

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

HTML экранирование

Стандартными инструментами для выхода из HTML являются шаблоны, такие как Jinja . Основным преимуществом является то, что они предназначены для экранирования текста по умолчанию , вместо того, чтобы требовать от вас явного преобразования небезопасных строк. (Тем не менее, вы должны быть осторожны, обходя или отключая даже временное экранирование. Я видел свою долю небезопасных попыток небезопасного построения JSON в шаблонах, но риск в шаблонах все же ниже, чем в системе, требующей явного экранирования везде.) Ваш пример довольно легко реализовать с помощью Jinja:

import jinja2

template_str = 'Hi, {{name}}'
name = "Bobby <img src=x onerr=alert(1)></img> Arson"

jinjaenv = jinja2.Environment(autoescape=jinja2.select_autoescape(['html', 'xml']))
template = jinjaenv.from_string(template_str)

print(template.render(name=name))
# Hi, Bobby &lt;img src=x onerr=alert(1)&gt;&lt;/img&gt; Arson

Однако, если вы генерируете HTML, скорее всего, вы используете веб-фреймворк, такой как Flask или Django. Эти фреймворки включают шаблонизатор и требуют меньше настроек, чем приведенный выше пример.

MarkupSafe - это полезный инструмент, если вы пытаетесь создать свой собственный шаблонизатор (некоторые движки шаблонов Python используют его внутри, например, Jinja.), И вы можете потенциально интегрировать его с Formatter. Но нет причин изобретать велосипед. Использование популярного движка приведет к гораздо более простому и понятному коду.

SQL-инъекция

SQL-инъекция не решается через экранирование. У PHP неприятная история , из которой все учились. Урок использовать параметризованные запросы вместо попытки избежать ввода. Это предотвращает синтаксический анализ ненадежных пользовательских данных в виде кода SQL.

То, как вы это сделаете, зависит от того, какие именно библиотеки вы используете для выполнения ваших запросов, но, например, выполнение этого с помощью execute метода в SQLAlchemy выглядит следующим образом :

session.execute(text('SELECT * FROM thing WHERE id = :thingid'), thingid=id)

Обратите внимание, что SQLAlchemy имеет значение , а не , просто экранируя текст id, чтобы убедиться, что он не содержит код атаки. Это на самом деле различие между SQL и значением для сервера базы данных. База данных будет анализировать текст запроса как запрос, а затем будет включать значение отдельно после запроса, который был проанализирован. Это делает невозможным для значения id запускать непреднамеренные побочные эффекты.

Обратите внимание, что проблемы с цитированием исключаются параметризованными запросами:

name = 'blah blah blah'
session.execute(text('SELECT * FROM thing WHERE name = :thingname'), thingname=name)

Если вы не можете параметризовать, белый список в памяти

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

# This could also be loaded dynamically if needed.
valid_tables = {
    # Keys are uppercased for look up
    'TABLE1' : 'table1',
    'TABLE2': 'Table2',
    'TABLE3': 'TaBlE3',
    ...
}

def get_table_name(table_num):
    table_name = 'TABLE' + table_num
    try:
        return valid_tables[table_name]
    except KeyError:
        raise 'Unknown table number: ' + table_num


def query_for_thing(session, table_num):
    return session.execute(text('SELECT * FROM "{}"'.format(get_table_name(table_num))

Дело в том, что вы никогда не хотите, чтобы пользовательский ввод входил в ваш запрос как нечто отличное от параметра.

Убедитесь, что этот белый список присутствует в памяти приложения . Не выполняйте белый список в самом SQL. Белый список в SQL слишком поздно; к тому времени входные данные уже были проанализированы как SQL, что позволило бы инициировать атаки до того, как белый список вступит в силу.

Убедитесь, что вы понимаете свою библиотеку

В комментариях вы упомянули PySpark.Вы уверены, что делаете это правильно?Если вы создаете фрейм данных с использованием более простого SELECT * FROM thing, а затем используете функции фильтрации PySpark, уверены ли вы, что он не передает эти фильтры должным образом до запроса, исключая необходимость форматировать значения в нем без параметров?

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

С небольшими данными, просто фильтровать в памяти

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

filter_name = 'blah blah blah'
results = session.execute(text('SELECT * FROM thing'))
filtered_results = [r for r in results if r.name == filter_name]

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

Если вы застряли без поддержки параметризованных запросов, последним средством будет очень строгие ограничения на входные данные

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

Делать следующее НЕ рекомендуется. Я включаю его только в качестве абсолютного последнего средства. Не делайте этого, если у вас есть какой-либо другой выбор, и тратьте столько времени, сколько сможете (даже, скажу, пару недель исследований), пытаясь избежать этого.Для этого требуется очень высокий уровень усердия со стороны каждого вовлеченного члена команды, и большинство разработчиков не имеют такого уровня усердия.

Если ни один из вышеперечисленных не представляется возможным, тоСледующим подходом может быть все, что вы можете сделать:

Не запрашивать текстовые строки, поступающие от пользователя. Невозможно сделать это безопасным.Никакое количество цитирования, экранирования или ограничения не гарантируется.Я не знаю всех подробностей, но я читал о существовании злоупотреблений Юникодом, которые могут позволить обойти ограничения символов и тому подобное.Это просто не стоит того, чтобы попробовать.Единственные разрешенные текстовые строки должны быть внесены в белый список в памяти приложения (в отличие от белого списка с помощью некоторой функции SQL или базы данных).Обратите внимание, что даже использование функций цитирования на уровне базы данных (например, quote_literal в PostgreSQL) или хранимых процедур не может вам здесь помочь, потому что текст должен быть проанализирован как SQL, чтобы даже достичь этих функций, что позволило бы вызывать атаки до появления белого списка.может вступить в силу.

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

Вот пример для даты:

from datetime import datetime

def fetch_data(start_date, end_date):
    # Check data types to prevent injections
    if not isinstance(start_date, datetime):
        raise ValueError('start_date must be a datetime')
    if not isinstance(end_date, datetime):
        raise ValueError('end_date must be a datetime')

    # WARNING: Using format with SQL queries is bad practice, but we don't
    # have a choice because [client lib] doesn't support parameterized queries.
    # To mitigate this risk, we do not allow arbitrary strings as input.
    # We tightly control the input's data type (to something other than text or binary) and the format used in the query.
    session.execute(text(
        "SELECT * FROM thing WHERE timestamp BETWEEN CAST('{start}' AS TIMESTAMP) AND CAST('{end}' AS TIMESTAMP)"
        .format(
            # Make the format used explicit
            start=start_date.strftime('%Y-%m-%dT%H:%MZ'),
            end=end_date.strftime('%Y-%m-%dT%H:%MZ')
        )
    ))

user_input_start_date = '2019-05-01T00:00'
user_input_end_date = '2019-06-01T00:00'

parsed_start_date = datetime.strptime(user_input_start_date, "%Y-%m-%dT%H:%M")
parsed_end_date = datetime.strptime(user_input_end_date, "%Y-%m-%dT%H:%M")


data = fetch_data(parsed_start_date, parsed_end_date)

Есть несколько деталей, о которых вам нужно знать.

  1. Обратите внимание, что в той же функции, что и запрос , мы проверяем данныетип.Это одно из редких исключений в Python, где вы не хотите доверять типизации утки.Это функция безопасности, которая гарантирует, что незащищенные данные не будут случайно переданы в вашу функцию.
  2. Формат, переданный для ввода, когда он отображается в строке SQL, является явным.Опять же, речь идет о контроле и белых списках.Не оставляйте это на усмотрение какой-либо другой библиотеки, чтобы решить, в каком формате будет отображаться вход;убедитесь, что вы точно знаете , в каком формате вы можете быть уверены, что инъекции невозможны.Я совершенно уверен, что с форматом даты / времени ISO 8601 нет возможности внедрения, но я не подтвердил это явно.Вы должны подтвердить это.
  3. Цитирование значений производится вручную. Это нормально. И причина, по которой это нормально, заключается в том, что вы знаете, с какими типами данных вы имеете дело , и вы точно знаете, как будет выглядеть строка после ее форматирования.Это сделано специально: вы поддерживаете очень строгий, очень строгий контроль над форматом ввода для предотвращения инъекций.Вы знаете, нужно ли добавлять кавычки или нет на основе этого формата.
  4. Не пропустите комментарий о том, насколько плоха эта практика.Вы не представляете, кто будет читать этот код позже и какими знаниями или способностями они обладают.Компетентные разработчики, которые понимают риски безопасности здесь, оценят предупреждение;Разработчики, которые не были осведомлены, будут предупреждены о необходимости использования параметризованных запросов, когда это возможно, и о том, чтобы неосторожно включать новые условия.Если это вообще возможно, потребуйте, чтобы изменения в этих областях кода были рассмотрены дополнительными разработчиками для дальнейшего снижения рисков.
  5. Эта функция должна иметь полный контроль над генерацией запроса.Он не должен делегировать свою конструкцию другим функциям.Это связано с тем, что проверка типов данных должна проводиться очень, очень близко к построению запроса, чтобы избежать ошибок.

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

Я также отмечу, что код вызывающей стороны может принимать пользовательский ввод в любом удобном формате и анализировать его, используя любые инструменты, которые вы пожелаете.Это одно из преимуществ, требующих выделенный тип данных вместо строк для ввода: вы не привязываете абонентов к определенному формату строки, только к типу данных.В частности, для даты / времени вы можете рассмотреть некоторые сторонние библиотеки.

Вот еще один пример с десятичным значением:

from decimal import Decimal

def fetch_data(min_value, max_value):
    # Check data types to prevent injections
    if not isinstance(min_value, Decimal):
        raise ValueError('min_value must be a Decimal')
    if not isinstance(max_value, Decimal):
        raise ValueError('max_value must be a Decimal')

    # WARNING: Using format with SQL queries is bad practice, but we don't
    # have a choice because [client lib] doesn't support parameterized queries.
    # To mitigate this risk, we do not allow arbitrary strings as input.
    # We tightly control the input's data type (to something other than text or binary) and the format used in the query.
    session.execute(text(
        "SELECT * FROM thing WHERE thing_value BETWEEN CAST('{minv}' AS NUMERIC(26, 16)) AND CAST('{maxv}' AS NUMERIC(26, 16))"
        .format(
            # Make the format used explicit
            # Up to 16 decimal places. Maybe validate that at start of function?
            minv='{:.16f}'.format(min_value),
            maxv='{:.16f}'.format(max_value)
        )
    ))

user_input_min = '78.887'
user_input_max = '89789.78878989'

parsed_min = Decimal(user_input_min)
parsed_max = Decimal(user_input_max)

data = fetch_data(parsed_min, parsed_max)

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

2 голосов
/ 15 июня 2019

Вам не нужно приносить свой собственный форматер, если вы используете Python 3.6 или новее.В Python 3.6 представлены отформатированные строковые литералы, см. PEP 498: Отформатированные строковые литералы .

Ваш пример в Python 3.6 или новее будет выглядеть так:

name = "Bobby <img src=x onerr=alert(1)></img> Arson"
print(f"Hi, {name}")  # Hi, Bobby <img src=x onerr=alert(1)></img> Arson

The спецификация формата , которая может использоваться с str.format(), также может использоваться с форматированными строковыми литералами.

В этом примере

my_dict = {'A': 21.3, 'B': 242.12, 'C': 3200.53}

for key, value in my_dict.items():
    print(f"{key}{value:.>15.2f}")

выведет следующее:

A..........21.30
B.........242.12
C........3200.53

Кроме того, поскольку строка вычисляется во время выполнения, можно использовать любое допустимое выражение python,например,

name = "Abby"
print(f"Hello, {name.upper()}!")

напечатает

Hello, ABBY!
...