如何转义 f 字符串中的字段?

How can I escape fields in a f-string?

提问人:badp 提问时间:6/11/2019 更新时间:6/22/2019 访问量:1474

问:

Javascript 的 f-strings 版本允许通过使用一个有点有趣的 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 f-strings 是否允许类似的机制?或者您是否需要自带字符串。格式化程序?在插值之前,更 pythonic 的实现会使用覆盖方法将结果包装到类中吗?__str__()

python-3.x xss SQL注入

评论

0赞 badp 6/11/2019
YYYY-MM-DD 日期带给您的这个问题将被解析为 YYYY 减去 MM 减去 DD 如果您不引用它们联盟
0赞 Delari Jesus 6/14/2019
嗨,我认为这可以帮助你。清楚地识别 mu python 中 f-string 的操作 cito.github.io/blog/f-strings

答:

2赞 orangecaterpillar 6/15/2019 #1

如果您使用的是 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

可用于的格式规范也可以与格式化字符串文本一起使用。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!

评论

6赞 jpmc26 6/16/2019
作者试图通过用户输入的 HTML 注入来防止 XSS 攻击。你的例子没有这样做。它使攻击无法逃脱。
1赞 Aaron Bentley 6/22/2019
提问者已经知道格式化的字符串文字,并在问题标题中将它们称为“f-strings”,这是您引用的文档中也使用的别名。这个问题是关于对格式化字符串文字中的字段进行转义,因此您的答案并没有真正解决它。
10赞 11 revsjpmc26 #2

当您处理将要被解释为代码的文本(例如,浏览器将解析为 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 等 Web 框架。这些框架包括一个模板引擎,并且需要比上面的示例更少的设置。

如果您尝试创建自己的模板引擎(某些 Python 模板引擎在内部使用它,例如 Jinja.),MarkupSafe 是一个有用的工具,并且您可以将其与 .但是没有理由重新发明轮子。使用流行的引擎将产生更简单、更易于遵循、更易于识别的代码。Formatter

SQL注入

SQL注入不是通过转义来解决的。PHP有一个令人讨厌的历史,每个人都从中吸取了教训。本课是使用参数化查询,而不是尝试转义输入。这样可以防止将不受信任的用户数据解析为 SQL 代码。

如何执行此操作取决于您用于执行查询的确切库,但举个例子,使用 SQLAlchemy 的方法执行此操作如下所示:execute

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

请注意,SQLAlchemy 不仅仅是转义 的文本,以确保它不包含攻击代码。它实际上是在区分 SQL 和数据库服务器的值。数据库会将查询文本分析为查询,然后在分析查询单独包含该值。这使得 的值不可能触发意外的副作用。idid

另请注意,参数化查询排除了引用问题:

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。你确定你做对了吗?如果仅使用更简单的数据框创建数据帧,然后使用 PySpark 筛选函数,是否确定它不能正确地将这些筛选器向下推送到查询中,从而排除了将值格式化为未参数化的需要?SELECT * FROM thing

确保您了解通常如何使用库过滤和操作数据,并检查该机制是否使用参数化查询或以其他方式在后台足够高效。

对于小数据,只需在内存中过滤即可

如果您的数据至少不在数万条记录中,那么请考虑将其加载到内存中,然后进行筛选:

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

如果这足够快,并且很难参数化查询,那么这种方法可以避免尝试使输入安全的所有安全问题。使用比您在生产中看到的更多的数据来测试其性能。我会使用至少两倍于您期望的最大值;如果你能让它执行一个数量级,那就更安全了。

如果遇到没有参数化查询支持的情况,最后的手段是对输入进行非常严格的限制

如果遇到不支持参数化查询的客户端,请首先检查是否可以使用更好的客户端。没有参数化查询的 SQL 是荒谬的,这表明您使用的客户端质量非常低,并且可能没有得到很好的维护;它甚至可能没有被广泛使用。

不建议执行以下操作。我只把它作为绝对的最后手段。如果你有任何其他选择,不要这样做,并尽可能多地花时间(甚至几周的研究,我敢说)试图避免诉诸于此。它要求每个参与其中的团队成员都非常勤奋,而大多数开发人员都没有这种勤奋程度。

如果以上都不可能,那么以下方法可能就是你所能做的:

不要查询来自用户的文本字符串。没有办法让它安全。不保证引用、转义或限制的数量。我不知道所有的细节,但我读过Unicode滥用的存在,可以绕过字符限制等。只是不值得尝试。唯一允许的文本字符串应该在应用程序内存中列入白名单(而不是通过某些 SQL 或数据库函数列入白名单)。请注意,即使利用数据库级别的引用函数(如 PostgreSQL 的)或存储过程也无法帮助您,因为文本必须解析为 SQL 才能到达这些函数,这将允许在白名单生效之前调用攻击。quote_literal

对于所有其他数据类型,请先分析它们,然后让语言将它们呈现为适当的字符串。再次这样做意味着避免将用户输入解析为 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. 此函数应完全控制查询的生成。它不应将其构造委托给其他职能。这是因为数据类型检查需要非常非常接近查询的构造,以避免错误。

这样做的效果是一种更宽松的白名单技术。您无法将特定值列入白名单,但可以将正在使用的值类型列入白名单,并控制它们的传递格式。强制调用方将值解析为已知数据类型可降低攻击通过的可能性。

我还要注意的是,调用代码可以自由地接受任何方便格式的用户输入,并使用您想要的任何工具对其进行解析。这是需要专用数据类型而不是字符串进行输入的优点之一:您不会将调用方锁定为特定的字符串格式,而只是数据类型。特别是对于日期/时间,您可以考虑一些第三方库。

下面是另一个使用 Decimal 值的示例:

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)

一切都基本相同。只是数据类型和格式略有不同。当然,您可以自由使用数据库支持的任何数据类型。例如,如果数据库不需要在数值类型上指定小数位数和精度,或者会自动转换字符串,或者可以处理不加引号的值,则可以相应地构建查询。

评论

1赞 jpmc26 6/16/2019
如果对此事有任何混淆,我选择社区维基,因为我不确定问题的质量;它可能太宽泛了。因此,我希望放弃任何声誉收益。但是,由于赏金的原因,它无法关闭,并且我希望阻止不安全的编码行为,因此我觉得无论如何都必须发布答案。
0赞 badp 6/17/2019
如果它有助于限制我问题的广度,我想知道是否存在某种机制可以或可以内置到 f 字符串中,例如一些类似的东西(而不是 )。虽然我的问题比这更通用,但在触发我问题的情况下,我正在处理不支持参数化查询的大数据 HQL 查询:)f"select * from Export_{yyyymm} where purchase_date between {since|quote} and {until|quote}"between "{since}" and "{until}"
0赞 jpmc26 6/17/2019
@badp HQL 是指 Hive QL?您使用的是哪个客户端库?
1赞 jpmc26 6/21/2019
@badp 对于将来的问题,问题中需要包含阻止您使用标准方法解决问题的限制。如果你不这么说,回答者就不可能知道你甚至已经想到了他们,提供原因可以让回答者在存在误解时纠正你,或者在存在特定情况时参考标准方法。现在有两个答案,修改这个问题可能为时已晚,因为不鼓励无效的答案。
1赞 jpmc26 6/22/2019
@badp 我更新了另外两个部分。一种是检查PySpark的过滤功能在后台是如何工作的。他们可能会发出参数化查询或执行其他操作来确保筛选足够有效。另一个是关于试图保护非文本输入的长篇大论。我不建议这样做,我希望与该部分相关的长度、难度和烦恼是劝阻的。