Odoo 的安全性¶
除了使用自定义代码手动管理访问之外,Odoo 还提供两种主要的数据驱动机制来管理或限制对数据的访问。
这两种机制都通过“组”链接到特定用户:用户属于任意数量的组,安全机制与组相关联,从而将安全机制应用于用户。
访问权¶
*授予*对给定操作集的整个模型的访问权限。如果没有访问权限与用户(通过其组)对模型的操作相匹配,则该用户无权访问。
访问权限是累加性的,用户的访问权限是他们通过所有组(例如,组)获得的访问权限的并集。如果用户属于授予读取和创建访问权限的 A 组,以及授予更新访问权限的 B 组,则该用户将拥有创建、读取和更新的所有三个权限。
记录规则¶
记录规则是为了允许操作而必须满足的“条件”。记录规则按照访问权限逐条记录进行评估。
记录规则是默认允许的:如果访问权限授予访问权限并且没有规则应用于用户的操作和模型,则授予访问权限。
- class ir.rule¶
- name¶
规则的描述。
- model_id¶
规则适用的模型。
- groups¶
授予(或不授予)访问权限的
res.groups。可以指定多个组。如果未指定组,则规则为*全局*,其处理方式与“组”规则不同(见下文)。
perm_method与ir.model.access具有完全不同的语义:对于规则,它们指定规则适用于*哪个操作。如果未选择某个操作,则不会检查该操作的规则,就好像该规则不存在一样。默认选择所有操作。
- perm_create¶
- perm_read¶
- perm_write¶
- perm_unlink¶
全局规则与组规则¶
全局规则和组规则在如何组成和组合方面存在很大差异:
全局规则*相交*,如果应用两个全局规则,则必须满足*两个*才能授予访问权限,这意味着添加全局规则始终会进一步限制访问。
组规则*统一*,如果应用两个组规则,则可以满足*任一*即可授予访问权限。这意味着添加组规则可以扩展访问权限,但不能超出全局规则定义的范围。
全局规则集和组规则集*相交*,这意味着添加到给定全局规则集的第一个组规则将限制访问。
危险
创建多个全局规则是有风险的,因为可能会创建不重叠的规则集,这将删除所有访问权限。
现场访问¶
ORM Field 可以具有提供组列表的 groups 属性(作为逗号分隔的 external identifiers 字符串)。
如果当前用户不在列出的组之一中,他将无权访问该字段:
受限字段会自动从请求的视图中删除
受限字段已从
fields_get()响应中删除尝试(显式)读取或写入受限字段会导致访问错误
安全陷阱¶
作为开发人员,了解安全机制并避免导致不安全代码的常见错误非常重要。
不安全的公共方法¶
任何公共方法都可以通过 RPC call 使用所选参数执行。以“_”开头的方法不能从操作按钮或外部 API 调用。
在公共方法上,执行方法的记录和参数不可信,ACL 仅在 CRUD 操作期间验证。
# this method is public and its arguments can not be trusted
def action_done(self):
if self.state == "draft" and self.env.user.has_group('base.manager'):
self._set_state("done")
# this method is private and can only be called from other python methods
def _set_state(self, new_state):
self.sudo().write({"state": new_state})
将方法设为私有显然是不够的,必须小心正确使用它。
绕过 ORM¶
当 ORM 可以做同样的事情时,你永远不应该直接使用数据库游标!通过这样做,您将绕过所有 ORM 功能,可能会绕过自动行为,例如翻译、字段失效、“active”、访问权限等。
而且很可能您还使代码更难阅读并且可能更不安全。
# very very wrong
self.env.cr.execute('SELECT id FROM auction_lots WHERE auction_id in (' + ','.join(map(str, ids))+') AND state=%s AND obj_price > 0', ('draft',))
auction_lots_ids = [x[0] for x in self.env.cr.fetchall()]
# no injection, but still wrong
self.env.cr.execute('SELECT id FROM auction_lots WHERE auction_id in %s '\
'AND state=%s AND obj_price > 0', (tuple(ids), 'draft',))
auction_lots_ids = [x[0] for x in self.env.cr.fetchall()]
# nearly there
auction_lots_ids = [x[x] for x in self.env.execute_query(SQL("""
SELECT id FROM auction_lots
WHERE auction_id IN %s AND state = %s AND obj_price > 0
""", tuple(ids), 'draft'))
# better
auction_lots_ids = self.search([('auction_id','in',ids), ('state','=','draft'), ('obj_price','>',0)])
SQL注入¶
使用手动 SQL 查询时必须注意不要引入 SQL 注入漏洞。当用户输入被错误过滤或错误引用时,就会出现该漏洞,从而允许攻击者在 SQL 查询中引入不需要的子句(例如绕过过滤器或执行 UPDATE or DELETE 命令)。
确保安全的最佳方法是永远、永远不要使用 Python 字符串连接 (+) 或字符串参数插值 (%) 将变量传递给 SQL 查询字符串。
第二个原因几乎同样重要,是数据库抽象层 (psycopg2) 的工作是决定如何格式化查询参数,而不是您的工作!例如,psycopg2 知道当您传递值列表时,它需要将它们格式化为逗号分隔的列表,并用括号括起来!
更好的是,存在一个 SQL 包装器,可以使用处理输入格式的模板来构建查询。检查 SQL execution 了解详细用法。
# the following is very bad:
# - it's a SQL injection vulnerability
# - it's unreadable
# - it's not your job to format the list of ids
self.env.cr.execute('SELECT distinct child_id FROM account_account_consol_rel ' +
'WHERE parent_id IN ('+','.join(map(str, ids))+')')
# better
self.env.cr.execute('SELECT DISTINCT child_id '\
'FROM account_account_consol_rel '\
'WHERE parent_id IN %s',
(tuple(ids),))
# more readable
self.env.cr.execute(SQL("""
SELECT DISTINCT child_id
FROM account_account_consol_rel
WHERE parent_id IN %s
""", tuple(ids)))
这非常重要,所以重构时也请小心,最重要的是不要复制这些模式!
这是一个令人难忘的示例,可帮助您记住问题的含义(但不要复制那里的代码)。在继续之前,请务必阅读 pyscopg2 的在线文档以了解如何正确使用它:
构建域¶
域以列表的形式表示并且是可序列化的。您可能会倾向于直接操作这些列表,但是它可能会引入微妙的问题,如果输入未标准化,用户可能会注入域。使用 Domain 安全地处理域的操作。
# bad
# the user can just pass ['|', ('id', '>', 0)] to access all
domain = ... # passed by the user
security_domain = [('user_id', '=', self.env.uid)]
domain += security_domain # can have a side-effect if this is a function argument
self.search(domain)
# better
domain = Domain(...)
domain &= Domain('user_id', '=', self.env.uid)
self.search(domain)
未转义的字段内容¶
当使用 JavaScript 和 XML 呈现内容时,人们可能会想使用 t-raw to display rich-text content. This should be avoided as a frequent XSS 向量。
从计算到最终集成到浏览器 DOM 中,控制数据的完整性非常困难。在引入时正确转义的 t-raw 在下一次错误修复或重构时可能不再安全。
QWeb.render('insecure_template', {
info_message: "You have an <strong>important</strong> notification",
})
<div t-name="insecure_template">
<div id="information-bar"><t t-raw="info_message" /></div>
</div>
上面的代码可能感觉很安全,因为消息内容是受控制的,但这是一种不好的做法,一旦该代码将来发生变化,可能会导致意外的安全漏洞。
// XSS possible with unescaped user provided content !
QWeb.render('insecure_template', {
info_message: "You have an <strong>important</strong> notification on " \
+ "the product <strong>" + product.name + "</strong>",
})
虽然以不同的方式格式化模板可以防止此类漏洞。
QWeb.render('secure_template', {
message: "You have an important notification on the product:",
subject: product.name
})
<div t-name="secure_template">
<div id="information-bar">
<div class="info"><t t-esc="message" /></div>
<div class="subject"><t t-esc="subject" /></div>
</div>
</div>
.subject {
font-weight: bold;
}
使用 Markup 创建安全内容¶
请参阅 official documentation 进行解释,但 Markup 的一大优点是它是一个非常丰富的类型,覆盖 str 操作以*自动转义参数*。
这意味着通过在字符串文字上使用 Markup 并“格式化”用户提供的(因此可能不安全)内容,可以轻松创建*安全* html 片段:
>>> Markup('<em>Hello</em> ') + '<foo>'
Markup('<em>Hello</em> <foo>')
>>> Markup('<em>Hello</em> %s') % '<foo>'
Markup('<em>Hello</em> <foo>')
尽管这是一件非常好的事情,但请注意,有时效果可能会很奇怪:
>>> Markup('<a>').replace('>', 'x')
Markup('<a>')
>>> Markup('<a>').replace(Markup('>'), 'x')
Markup('<ax')
>>> Markup('<a>').replace('>', 'x')
Markup('<ax')
>>> Markup('<a>').replace('>', '&')
Markup('<a&')
小技巧
大多数内容安全的 API 实际上都会返回 Markup 及其含义。
escape 方法(及其别名 html_escape)将 str 转换为 Markup 并转义其内容。它不会转义 Markup 对象的内容。
def get_name(self, to_html=False):
if to_html:
return Markup("<strong>%s</strong>") % self.name # escape the name
else:
return self.name
>>> record.name = "<R&D>"
>>> escape(record.get_name())
Markup("<R&D>")
>>> escape(record.get_name(True))
Markup("<strong><R&D></strong>") # HTML is kept
生成 HTML 代码时,将结构(标签)与内容(文本)分开非常重要。
>>> Markup("<p>") + "Hello <R&D>" + Markup("</p>")
Markup('<p>Hello <R&D></p>')
>>> Markup("%s <br/> %s") % ("<R&D>", Markup("<p>Hello</p>"))
Markup('<R&D> <br/> <p>Hello</p>')
>>> escape("<R&D>")
Markup('<R&D>')
>>> _("List of Tasks on project %s: %s",
... project.name,
... Markup("<ul>%s</ul>") % Markup().join(Markup("<li>%s</li>") % t.name for t in project.task_ids)
... )
Markup('Liste de tâches pour le projet <R&D>: <ul><li>First <R&D> task</li></ul>')
>>> Markup("<p>Foo %</p>" % bar) # bad, bar is not escaped
>>> Markup("<p>Foo %</p>") % bar # good, bar is escaped if text and kept if markup
>>> link = Markup("<a>%s</a>") % self.name
>>> message = "Click %s" % link # bad, message is text and Markup did nothing
>>> message = escape("Click %s") % link # good, format two markup objects together
>>> Markup(f"<p>Foo {self.bar}</p>") # bad, bar is inserted before escaping
>>> Markup("<p>Foo {bar}</p>").format(bar=self.bar) # good, sorry no fstring
在进行翻译时,将 HTML 与文本分开尤其重要。转换方法接受 Markup 参数,如果至少收到一个参数,则会转义转换。
>>> Markup("<p>%s</p>") % _("Hello <R&D>")
Markup('<p>Bonjour <R&D></p>')
>>> _("Order %s has been confirmed", Markup("<a>%s</a>") % order.name)
Markup('Order <a>SO42</a> has been confirmed')
>>> _("Message received from %(name)s <%(email)s>",
... name=self.name,
... email=Markup("<a href='mailto:%s'>%s</a>") % (self.email, self.email)
Markup('Message received from Georges <<a href=mailto:george@abitbol.example>george@abitbol.example</a>>')
逃避与消毒¶
重要
当您混合数据和代码时,无论数据多么安全,转义始终是 100% 强制的
**转义**将*TEXT* 转换为*CODE*。每次将 DATA/TEXT 与 CODE 混合时(例如生成要在 safe_eval 内评估的 HTML 或 python 代码),绝对必须执行此操作,因为 CODE 始终需要对 TEXT 进行编码。这对于安全性至关重要,但也是一个正确性问题。即使不存在安全风险(因为文本 100% 保证安全或可信),仍然需要它(例如,避免破坏生成的 HTML 中的布局)。
只要开发人员确定哪个变量包含 TEXT 以及哪个变量包含 CODE,转义就永远不会破坏任何功能。
>>> from odoo.tools import html_escape, html_sanitize
>>> data = "<R&D>" # `data` is some TEXT coming from somewhere
# Escaping turns it into CODE, good!
>>> code = html_escape(data)
>>> code
Markup('<R&D>')
# Now you can mix it with other code...
>>> self.website_description = Markup("<strong>%s</strong>") % code
清理**将*代码*转换为*安全代码*(但不是必需的*安全*代码)。它不适用于 *TEXT*。仅当 *CODE* 不受信任时才需要进行清理,因为它全部或部分来自某些用户提供的数据。如果用户提供的数据采用 *TEXT* 的形式(例如,用户填写的表单中的内容),并且如果该数据在放入 *CODE* 之前已正确转义,则清理是无用的(但仍然可以完成)。但是,如果用户提供的数据**未转义,那么清理将**不会**按预期进行。
# Sanitizing without escaping is BROKEN: data is corrupted!
>>> html_sanitize(data)
Markup('')
# Sanitizing *after* escaping is OK!
>>> html_sanitize(code)
Markup('<p><R&D></p>')
清理可能会破坏功能,具体取决于 CODE 是否预期包含不安全的模式。这就是为什么 fields.Html 和 tools.html_sanitize() 有选项来微调样式的清理级别等。必须根据数据的来源和所需的功能仔细考虑这些选项。消毒安全性与消毒损坏之间存在平衡:消毒越安全,损坏物品的可能性就越大。
>>> code = "<p class='text-warning'>Important Information</p>"
# this will remove the style, which may break features
# but is necessary if the source is untrusted
>>> html_sanitize(code, strip_classes=True)
Markup('<p>Important Information</p>')
评估内容¶
有些人可能希望不惜一切代价避免“eval` to parse user provided content. Using ``eval`”。可以使用更安全的沙盒方法 safe_eval 来代替,但仍然为运行该方法的用户提供了巨大的功能,并且只能为受信任的特权用户保留,因为它打破了代码和数据之间的障碍。
# very bad
domain = eval(self.filter_domain)
return self.search(domain)
# better but still not recommended
from odoo.tools import safe_eval
domain = safe_eval(self.filter_domain)
return self.search(domain)
# good
from ast import literal_eval
domain = literal_eval(self.filter_domain)
return self.search(domain)
解析内容不需要``eval``
语言 |
数据类型 |
合适的解析器 |
|---|---|---|
Python |
整型、浮点型等 |
整型()、浮点型() |
JavaScript |
整型、浮点型等 |
parseInt()、parseFloat() |
Python |
词典 |
json.loads(), ast.literal_eval() |
JavaScript |
对象、列表等 |
JSON.parse() |
访问对象属性¶
如果需要动态检索或修改记录的值,则可能需要使用“getattr` and ``setattr`”方法。
# unsafe retrieval of a field value
def _get_state_value(self, res_id, state_field):
record = self.sudo().browse(res_id)
return getattr(record, state_field, False)
然而,此代码并不安全,因为它允许访问记录的任何属性,包括私有属性或方法。
记录集的``__getitem__``已经被定义,并且可以轻松安全地访问动态字段值:
# better retrieval of a field value
def _get_state_value(self, res_id, state_field):
record = self.sudo().browse(res_id)
return record[state_field]
上述方法显然还是过于乐观,还必须对记录id和字段值进行额外的验证。