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 <img src=x onerr=alert(1)></img> Arson"
Run Code Online (Sandbox Code Playgroud)
Python f字符串是否允许类似的机制?还是您需要自带string.Formatter?__str__()插值之前,是否有更pythonic的实现将结果包装到具有重写方法的类中?
当您处理将被解释为代码的文本(例如,浏览器将解析为HTML的文本或数据库以SQL执行的文本)时,您不想通过实现自己的转义来解决安全性问题机制。您想使用经过广泛测试的标准工具来防止它们。出于以下几个原因,这使您免受攻击的安全性更高:
用于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
Run Code Online (Sandbox Code Playgroud)
但是,如果要生成HTML,则很可能使用的是诸如Flask或Django之类的Web框架。这些框架包括模板引擎,与上面的示例相比,所需的设置更少。
如果您尝试创建自己的模板引擎(某些Python模板引擎在内部使用它,例如Jinja),则MarkupSafe是有用的工具,并且可以将其与集成Formatter。但是没有理由重新发明轮子。使用流行的引擎将导致更简单,更易于遵循,更易于识别的代码。
无法通过转义来解决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)
您需要注意一些细节。
这样的效果是一种较宽松的白名单技术。您不能将特定的值列入白名单,但可以将正在使用的值的种类列入白名单,并控制其传递的格式。强制调用者将这些值解析为已知的数据类型可减少攻击通过的可能性。
我还要注意,调用者代码可以自由接受方便的任何格式的用户输入,并可以使用所需的任何工具对其进行解析。这是需要专用数据类型而不是字符串进行输入的优点之一:您不必将调用者锁定为特定的字符串格式,而只需锁定数据类型即可。特别是对于日期/时间,您可以考虑一些第三方库。
这是另一个使用十进制值的示例:
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)
一切基本相同。数据类型和格式略有不同。当然,您可以自由使用数据库支持的任何数据类型。例如,如果您的数据库不需要在数字类型上指定小数位数和精度,或者将自动转换字符串或可以处理未引用的值,则可以相应地构造查询。
如果您使用的是 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)
| 归档时间: |
|
| 查看次数: |
293 次 |
| 最近记录: |