如何在F字符串中转义字段?

bad*_*adp 7 xss sql-injection python-3.x

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"
Run Code Online (Sandbox Code Playgroud)

Python f字符串是否允许类似的机制?还是您需要自带string.Formatter__str__()插值之前,是否有更pythonic的实现将结果包装到具有重写方法的类中?

jpm*_*c26 9

当您处理将被解释为代码的文本(例如,浏览器将解析为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
Run Code Online (Sandbox Code Playgroud)

但是,如果要生成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)
Run Code Online (Sandbox Code Playgroud)

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

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

name = 'blah blah blah'
session.execute(text('SELECT * FROM thing WHERE name = :thingname'), thingname=name)
Run Code Online (Sandbox Code Playgroud)

如果无法参数化,请在内存中添加白名单

有时,无法对某些参数进行参数化。也许您正在尝试根据输入动态选择表名。在这些情况下,您可以做的一件事情就是收集已知的有效和安全值。通过验证输入是这些值之一并检索到它的已知安全表示,可以避免将用户输入发送到查询中:

# 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))
Run Code Online (Sandbox Code Playgroud)

关键是您永远不想让用户输入作为参数以外的内容进入查询。

确保此白名单出现在应用程序内存中。不要在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]
Run Code Online (Sandbox Code Playgroud)

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

如果你坚持不参数化查询的支持,最后的手段是非常的投入严格的限制

如果您不支持不支持参数化查询的客户端,请首先检查是否可以使用更好的客户端。没有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)
Run Code Online (Sandbox Code Playgroud)

您需要注意一些细节。

  1. 注意,在与query相同的函数中,我们正在验证数据类型。这是Python中罕见的例外之一,您不想信任Duck类型。这是一项安全功能,可确保不安全的数据不会意外传递到您的函数中。
  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)
Run Code Online (Sandbox Code Playgroud)

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


ora*_*lar 2

如果您使用的是 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
Run Code Online (Sandbox Code Playgroud)

可以与 一起使用的格式规范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}")
Run Code Online (Sandbox Code Playgroud)

将打印以下内容:

A..........21.30
B.........242.12
C........3200.53
Run Code Online (Sandbox Code Playgroud)

此外,由于字符串是在运行时计算的,因此可以使用任何有效的 python 表达式,例如,

name = "Abby"
print(f"Hello, {name.upper()}!")
Run Code Online (Sandbox Code Playgroud)

将打印

Hello, ABBY!
Run Code Online (Sandbox Code Playgroud)

  • 作者试图通过用户输入的 HTML 注入来防止 XSS 攻击。你的例子并没有做到这一点。它使攻击无法逃脱。 (6认同)