gpt4 book ai didi

python-3.x - 如何在F字符串中转义字段?

转载 作者:行者123 更新时间:2023-12-02 18:48:10 24 4
gpt4 key购买 nike

Javascript的f字符串版本允许通过使用一些有趣的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字符串是否允许类似的机制?还是您需要带上自己的 string.Formatter?插值之前,是否有更pythonic的实现将结果包装到具有重写的 __str__()方法的类中?

最佳答案

当您处理将被解释为代码的文本(例如,浏览器将解析为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有一个 nasty history,每个人都从中学到了。本课是使用参数化查询,而不是尝试转义输入。这样可以防止将不受信任的用户数据解析为SQL代码。

具体执行方式取决于执行查询所使用的库,例如,使用SQLAlchemy的 execute方法 looks like this

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]


如果这足够快并且很难对查询进行参数化,则此方法避免了尝试使输入变得安全的所有安全难题。使用比您期望在产品中看到的更多的数据来测试其性能。我至少会使用您期望的最大值的两倍;如果您可以执行一个数量级的命令,它将更加安全。

如果您在没有参数化查询支持的情况下陷入困境,那么最后的选择就是严格限制输入

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

不建议执行以下操作。我将其仅作为绝对的最后手段。如果您有其他选择,请不要这样做,并且要花尽可能多的时间(我敢说即使是几周的研究)也要尽量避免诉诸于此。每个参与团队的成员都需要非常高的勤奋水平,而大多数开发人员却没有这种勤奋水平。

如果以上都不是,则可以采取以下方法:

不要查询来自用户的文本字符串。没有办法确保此安全。不能保证报价,转义或限制的数量。我不知道所有的细节,但是我已经读过Unicode滥用的存在,这些滥用可以绕过字符限制等。只是不值得尝试。允许的唯一文本字符串应在应用程序内存中列入白名单(而不是通过某些SQL或数据库功能列入白名单)。请注意,即使利用数据库级报价功能(如PostgreSQL的 quote_literal)或存储过程也无法为您提供帮助,因为必须将文本解析为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)


您需要注意一些细节。


注意,在与查询相同的功能中,我们正在验证数据类型。这是Python中罕见的例外之一,您不想信任Duck类型。这是一项安全功能,可确保不会将不安全的数据意外传递到您的函数中。
输入呈现为SQL字符串时传递的格式是显式的。同样,这与控制和列入白名单有关。不要将其留给任何其他库来决定输入将呈现为什么格式;确保确切知道格式是什么,以便可以确定不可能进行注入。我相当确定,ISO 8601日期/时间格式没有注入的可能性,但是我还没有明确证实。您应该确认这一点。
值的引用是手动的。没关系。没问题的原因是因为您知道要处理的数据类型,并且知道字符串格式化后的确切样子。这是设计使然:您将对输入格式保持非常严格,严格的控制,以防止注入。您知道是否需要根据该格式添加引号。
不要忽略有关这种做法有多糟糕的评论。您不知道谁稍后会阅读此代码以及他们拥有什么知识或能力。有能力的开发人员如果了解此处的安全风险,将不胜感激。警告那些不了解的开发人员,只要有可用,就使用参数化查询,并避免粗心地引入新条件。如果完全可行,则要求其他开发人员审查对这些代码区域的更改,以进一步减轻风险。
此功能应完全控制生成查询。它不应将其构造委托给其他功能。这是因为数据类型检查需要与查询的结构保持非常非常紧密的关系以避免错误。


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

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

这是另一个使用十进制值的示例:

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)


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

关于python-3.x - 如何在F字符串中转义字段?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56544119/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com