Security in Odoo (Chinese translation)


In addition to manually managing access with custom code, Odoo provides two main data-driven mechanisms to manage or restrict access to data.

Both mechanisms are linked to specific users via groups: users belong to any number of groups, and security mechanisms are associated with groups, thereby applying security mechanisms to users

class res.groups

  • name
    as a user-readable group identifier (to clarify the role/purpose of the group)
  • The category_id
    module category is used to associate groups with Odoo applications (~a set of related business models) and convert them into exclusive selections in user forms
  • implied_ids
    other groups set with this user. This is a convenient pseudo-inheritance relationship: the implicit group can be explicitly removed from the user without removing the implicit group.
  • comment
    A supplementary note about the group, for example

Access Rights

Grants access to the entire model for the given set of operations. If no access rights match the user's (via its groups) actions on the model, then the user has no access rights.

Access rights are additive, a user's access rights are the union of their access rights in all groups, for example, given a user belongs to group A which grants read and create access, and belongs to group B which grants update access, then The user will have create, read, and update permissions.

class ir.model.access


  • the purpose or role of the name group
  • model_id
    the model whose access is controlled by the ACL
  • group_id
    to grant access to res.groups, an empty group_id means to grant the ACL to each user (non-employees, such as portal or public users)

The perm_method attribute grants the corresponding CRUD access permissions when set, by default they are not set

  • perm_create
  • perm_read
  • perm_write
  • perm_unlink

Record Rules

Recording rules are conditions that must be met in order to allow an action. Record rules are evaluated on a record-by-record basis by access rights.

The record rule is default-allow: if the access permission allows access, and there are no rules for the user's action and model, then grant access

class ir.rule


  • Description of the name rule

  • The model to which the model_id
    rule applies

  • groups
    res.group to grant (or not grant) access to. Multiple groups can be specified. If no group is specified, the rule is global and is treated differently from the 'group' rule (see below).

  • global
    is computed on a group basis, providing easy access to the global state (or non-global state) of the rule

  • domain_force
    as a predicate specified as a domain, if the domain matches the record, the selected action is allowed, otherwise the selected action is forbidden

    A domain is a python expression that can use the following variables

    • time
      Python's time module
    • user
      The current user, as a singleton recordset
    • company_id
      the company currently selected by the current user as a single company id (not a recordset)
    • company_ids
      All companies that the current user has access to as a list of company ids (not a recordset), see Security Rules for more details

    perm_method has completely different semantics than ir.model.access: for rules, they specify which operation the rule applies to. If an action is not selected, the rule is not checked as if the rule did not exist

    All actions are selected by default

  • perm_create

  • perm_read

  • perm_write

  • perm_unlink

Global rules versus group rules

Global rules and group rules differ significantly in how they are composed and combined

  • Global rules intersect, if two global rules are applied, both rules must be satisfied to grant access, which means that adding a global rule always restricts access further
  • Group rules are unified, if both group rules apply, either one can be satisfied to grant access. This means that adding group rules can extend access, but not beyond the scope defined by the global rules
  • Global and group rule sets intersect, meaning that the first group rule added to a given global rule set will restrict access

DANGER
Creating multiple global rules is risky as non-overlapping rule sets may be created which will remove all access

Field Access (field access / domain access)

ORM fields can have a groups attribute, providing a list of groups (as a comma-separated string of external identifiers).

If the current user is not in the listed groups, he will not have access to the field:

  • Restricted fields are automatically removed from the requested view
  • Restricted fields will be removed from the fields_get() response
  • Attempts to (explicitly) read or write a restricted field will result in an access error

Security Pitfalls

As a developer, it's important to understand security mechanisms and avoid common mistakes that lead to unsafe code

Unsafe Public Methods (unsafe public methods)

Any public method can be executed via an RPC call with selected parameters. Methods starting with _ cannot be called from action buttons or external APIs.

On public methods, records and parameters of executing methods cannot be trusted, ACLs are only validated during CRUD operations

# this method is public and its arguments can not be trusted
def action_done(self):
    if self.state == "draft" and self.user_has_groups('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})

Making a method private is obviously not enough, care must be taken to use it correctly.

Bypassing the ORM

Never use database cursors directly when an ORM can do the same! By doing this you bypass all ORM functionality, possibly automatic behavior like translations, invalidation of fields, events, access rights, etc.

Also, you may be making the code harder to read, and possibly less secure.

# 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 injections

Care must be taken not to introduce SQL injection vulnerabilities when using manual SQL queries. The vulnerability arises when user input is incorrectly filtered or improperly quoted, allowing an attacker to introduce undesired clauses in SQL queries, such as bypassing filters or executing UPDATE or DELETE commands.

The best way to be safe is to never pass variables into SQL query strings using Python string concatenation (+) or string parameter interpolation (%).

The second, almost equally important, reason is that it is the job of the database abstraction layer (psycopg2) to decide how to format query parameters, not your job! For example, psycopg2 knows that when you pass a list of values, it needs to format them as into a comma-separated list and enclose it in parentheses!

# 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),))

This is very important, so also be careful when refactoring, and above all don't copy the patterns!
Here's a memorable example to help you remember what the question is about (but don't copy the code). Before continuing, be sure to read the online documentation for pyscopg2 to learn how to use it properly:

  • Question about query parameters
  • How to pass parameters with psycopg2
  • Advanced parameter type
  • Psycopg documentation

Unescaped field content (unescaped field content)

When rendering content using JavaScript and XML, one might try to use t-raw to display rich text content. It should be avoided as a frequently used XSS vector.

It is very difficult to control the integrity of data from computation to final integration into the browser DOM. t-raw that was properly escaped when it was introduced may no longer be safe by the next bugfix or refactor

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>

The above code may feel secure due to the controlled message content, but this is a bad practice and could lead to unintended security holes should this code evolve in the future

// 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>",
})

Formatting templates differently prevents such vulnerabilities.

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;
}

Escaping vs Sanitizing (escape vs sanitizing)

IMPORTANT
When you mix data and code, no matter how safe the data is, escaping is always 100% mandatory

Escaping turns text into code. You have to do this every time you mix DATA/TEXT with CODE (e.g. to generate HTML or python code to be evaluated in safe_eval), because CODE always needs to encode TEXT. This is critical for security, but it's also a correctness issue. Even if there is no security risk (because the text is 100% guaranteed to be safe or trusted), it is still necessary (eg to avoid breaking the layout of the generated HTML).

Escaping never breaks any features, as long as the developer determines which variable contains TEXT and which contains CO

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
'&lt;R&amp;D&gt;'

# Now you can mix it with other code...
self.message_post(body="<strong>%s</strong>" % code)

Sanitization transforms code into safer code (but not necessarily safe code). It doesn't work with TEXT. Sanitization is only required if the CODE is not trusted, because it comes in whole or in part from some user-provided data. If the user-supplied data is in TEXT form (e.g. the contents of a form filled out by the user), and if that data is properly escaped before being put into CODE, sanitization is useless (but can still be done). However, if the user-supplied data is not escaped, sanitization will not work as expected

# Sanitizing without escaping is BROKEN: data is corrupted!
>>> html_sanitize(data)
''

# Sanitizing *after* escaping is OK!
>>> html_sanitize(code)
'<p>&lt;R&amp;D&gt;</p>'

Cleanup may break features, depending on whether the code contains unsafe patterns. That's why fields.Html and tools.html_sanitize() have options to fine-tune the level of sanitization for styles etc. These options must be carefully considered based on the source of the data and the desired functionality. The safety of sanitation is balanced against the damage of sanitation: the safer the sanitization, the more likely it is to break things

>>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)
'<p>Important Information</p>'

Evaluating content

Some people may wish to parse user-supplied content via eval. eval should be avoided anyway. A more secure sandboxed method safe_eval could be used, but it still provides tremendous power to the user running it and must be reserved only for trusted privileged users as it breaks the barrier between code and data

# 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)

Parsing content does not require eval
insert image description here

Accessing object attributes

If you need to dynamically retrieve or modify a record's value, you may want to use the getattr and setattr methods

# 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)

However, this code is not safe because it allows access to any property of the record, including private properties or methods.

A recordset's __getitem__ is already defined and provides easy and safe access to dynamic field values:

# 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]

The above method is obviously still too optimistic, and additional validation must be performed on the record id and field value

Guess you like

Origin blog.csdn.net/weixin_44141284/article/details/130921643