Odoo中的安全性¶
除了使用自定义代码手动管理访问权限外,Odoo 还提供了两种主要的数据驱动机制来管理或限制对数据的访问。
两种机制都通过 组 与特定用户关联:一个用户可以属于任意数量的组,安全机制与组相关联,从而将安全机制应用于用户。
- class res.groups¶
- name¶
为用户提供可读的组标识(拼写出组的角色/目的)
- category_id¶
The module category, serves to associate groups with an Odoo App (~a set of related business models) and convert them into an exclusive selection in the user form.
- implied_ids¶
在此用户旁边设置的其他组。这是一种方便的伪继承关系:可以明确地从用户中删除隐含的组,而不必删除隐含者。
- comment¶
关于组的附加说明,例如:
访问权限¶
*授予*给定一组操作对整个模型的访问权限。如果没有访问权限与用户(通过他们的组)在模型上的操作匹配,用户将无法访问。
访问权限是可叠加的,用户的访问权限是他们通过所有组获得的访问权限的并集,例如,给定一个用户属于组A,授予读取和创建访问权限,以及一个组B授予更新访问权限,用户将拥有创建、读取和更新的所有三个权限。
记录规则¶
记录规则是必须满足的 条件 ,才能允许操作。记录规则按记录逐个评估,遵循访问权限。
记录规则是默认允许的:如果访问权限授予访问权并且对于用户的操作和模型没有适用的规则,则授予访问权限。
- class ir.rule¶
- name¶
规则的描述。
- model_id¶
适用规则的模型。
- groups¶
授予(或不授予)访问权限的
res.groups
。可以指定多个组。如果未指定组,则规则是 全局 的,与“组”规则(见下文)不同对待。
The
perm_method
have completely different semantics than forir.model.access
: for rules, they specify which operation the rules applies for. If an operation is not selected, then the rule is not checked for it, as if the rule did not exist.默认情况下选择所有操作。
- perm_create¶
- perm_read¶
- perm_write¶
- perm_unlink¶
全局规则与组规则¶
全局规则和组规则在组合和合并方面存在很大的差异:
全局规则 相交 ,如果两个全局规则都适用,则 都 必须满足才能授予访问权限,这意味着添加全局规则总是进一步限制访问权限。
组规则 统一 ,如果两个组规则都适用,则可以满足 任一 规则以获得访问权限。这意味着添加组规则可以扩展访问权限,但不能超出全局规则所定义的范围。
全局和组规则集 相交,这意味着将第一个组规则添加到给定的全局规则集将限制访问。
危险
创建多个全局规则是有风险的,因为可能会创建不重叠的规则集,这将删除所有访问权限。
字段访问¶
ORM Field
可以有一个 groups
属性,提供了一组组(作为逗号分隔的 外部标识符 字符串)。
如果当前用户不属于列出的任何一个组,他将无法访问该字段:
受限字段将自动从请求的视图中删除
从
fields_get()
响应中移除了受限字段尝试(显式地)读取或写入受限字段会导致访问错误
安全陷阱¶
作为开发者,了解安全机制并避免常见的导致不安全代码的错误非常重要。
不安全的公共方法¶
任何公共方法都可以通过 RPC 调用 来执行,使用所选参数。以 _
开头的方法不能从操作按钮或外部 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()]
# better
auction_lots_ids = self.search([('auction_id','in',ids), ('state','=','draft'), ('obj_price','>',0)])
SQL注入攻击¶
使用手动 SQL 查询时,必须注意不要引入 SQL 注入漏洞。当用户输入被错误过滤或错误引用时,就会存在漏洞,允许攻击者向 SQL 查询中引入不必要的子句(例如绕过过滤器或执行 UPDATE
或 DELETE
命令)。
保持安全的最佳方法是永远不要使用Python字符串连接(+)或字符串参数插值(%)将变量传递给SQL查询字符串。
第二个原因,几乎同样重要的是,决定如何格式化查询参数的工作应该由数据库抽象层(psycopg2)来完成,而不是你的工作!例如,psycopg2 知道当你传递一个值列表时,它需要将它们格式化为逗号分隔的列表,括在括号中!
# 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),))
这非常重要,请在重构时小心,最重要的是不要复制这些模式!
这里有一个难忘的例子,帮助您记住问题所在(但不要复制那里的代码)。在继续之前,请确保阅读 pyscopg2 的在线文档,以了解如何正确使用它:
未转义的字段内容¶
当使用JavaScript和XML渲染内容时,人们可能会倾向于使用 t-raw
来显示富文本内容。这应该被避免,因为它是一个常见的 XSS <https://en.wikipedia.org/wiki/Cross-site_scripting>
_向量。
从计算到最终在浏览器 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
创建安全内容¶
See the official documentation for
explanations, but the big advantage of
Markup
is that it’s a very rich type overrinding
str
operations to automatically escape parameters.
这意味着通过在字符串文字上使用 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
,带有所有相关的内容。
The escape
method (and its
alias html_escape
) turns a str
into
a Markup
and escapes its content. It will not escape the
content of a Markup
object.
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
When generating HTML code, it is important to separate the structure (tags) from the content (text).
>>> 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
When working with translations, it is especially important to separate the HTML
from the text. The translation methods accepts a Markup
parameters and will escape the translation if it gets receives at least one.
>>> 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%必需的
Escaping converts TEXT to CODE. It is absolutely mandatory to do it
every time you mix DATA/TEXT with CODE (e.g. generating HTML or python code
to be evaluated inside a safe_eval
), because CODE always requires TEXT to
be encoded. It is critical for security, but it’s also a question of
correctness. Even when there is no security risk (because the text is 100%
guarantee to be safe or trusted), it is still required (e.g. to avoid breaking
the layout in generated 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
Sanitizing converts CODE to SAFER CODE (but not necessary safe code). It does not work on TEXT. Sanitizing is only necessary when CODE is untrusted, because it comes in full or in part from some user-provided data. If the user-provided data is in the form of TEXT (e.g. the content from a form filled by a user), and if that data was correctly escaped before putting it in CODE, then sanitizing is useless (but can still be done). If however, the user-provided data was not escaped, then sanitizing will not work as expected.
# 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
来解析用户提供的内容。但是应该尽量避免使用 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 |
整数,浮点数等。 |
int(), float() |
Javascript |
整数,浮点数等。 |
parseInt(),parseFloat() |
Python |
字典 |
json.loads(),ast.literal_eval() |
Javascript |
对象,列表等。 |
JSON.parse() |
访问对象属性¶
如果需要动态检索或修改记录的值,则可以使用 getattr
和 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和字段值进行额外的验证。