提问人:badp 提问时间:6/11/2019 更新时间:6/22/2019 访问量:1474
如何转义 f 字符串中的字段?
How can I escape fields in a f-string?
问:
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 <img src=x onerr=alert(1)></img> Arson"
Python f-strings 是否允许类似的机制?或者您是否需要自带字符串。格式化程序
?在插值之前,更 pythonic 的实现会使用覆盖方法将结果包装到类中吗?__str__()
答:
如果您使用的是 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!
评论
当您处理将要被解释为代码的文本(例如,浏览器将解析为 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 <img src=x onerr=alert(1)></img> 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 和数据库服务器的值。数据库会将查询文本分析为查询,然后在分析查询后单独包含该值。这使得 的值不可能触发意外的副作用。id
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。你确定你做对了吗?如果仅使用更简单的数据框创建数据帧,然后使用 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)
您需要注意几个细节。
- 请注意,在与查询相同的函数中,我们将验证数据类型。这是 Python 中罕见的例外之一,您不想信任鸭子类型。这是一项安全功能,可确保不安全的数据不会意外传递到您的函数中。
- 当输入呈现为 SQL 字符串时,输入传递的格式是显式的。同样,这是关于控制和白名单的。不要让任何其他库来决定将输入呈现为哪种格式;确保您确切地知道格式是什么,以便您可以确定注射是不可能的。我相当确定 ISO 8601 日期/时间格式没有注入的可能性,但我还没有明确确认这一点。您应该确认这一点。
- 值的引用是手动的。没关系。之所以没问题,是因为你知道你正在处理什么数据类型,并且你确切地知道字符串在格式化后会是什么样子。这是设计使然:您对输入的格式保持非常严格、非常严格的控制,以防止注入。您知道是否需要根据该格式添加引号。
- 不要跳过关于这种做法有多糟糕的评论。你不知道以后谁会阅读这段代码,也不知道他们有什么知识或能力。了解此处安全风险的有能力的开发人员会喜欢该警告;不知情的开发人员将被警告在可用时使用参数化查询,并避免粗心地包含新条件。如果可行,请要求其他开发人员审查对这些代码区域的更改,以进一步降低风险。
- 此函数应完全控制查询的生成。它不应将其构造委托给其他职能。这是因为数据类型检查需要非常非常接近查询的构造,以避免错误。
这样做的效果是一种更宽松的白名单技术。您无法将特定值列入白名单,但可以将正在使用的值类型列入白名单,并控制它们的传递格式。强制调用方将值解析为已知数据类型可降低攻击通过的可能性。
我还要注意的是,调用代码可以自由地接受任何方便格式的用户输入,并使用您想要的任何工具对其进行解析。这是需要专用数据类型而不是字符串进行输入的优点之一:您不会将调用方锁定为特定的字符串格式,而只是数据类型。特别是对于日期/时间,您可以考虑一些第三方库。
下面是另一个使用 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)
一切都基本相同。只是数据类型和格式略有不同。当然,您可以自由使用数据库支持的任何数据类型。例如,如果数据库不需要在数值类型上指定小数位数和精度,或者会自动转换字符串,或者可以处理不加引号的值,则可以相应地构建查询。
评论
f"select * from Export_{yyyymm} where purchase_date between {since|quote} and {until|quote}"
between "{since}" and "{until}"
评论