Odoo中的安全性?

除了使用自定義代碼手動管理訪問權限外,Odoo 還提供了兩種主要的數據驅動機制來管理或限制對數據的訪問。

這兩種機制都通過 與特定用戶相關聯:一個用戶屬于任意數量的組,安全機制與組相關聯,從而將安全機制應用于用戶。

class res.groups?
name?

為用戶提供可讀的組標識(拼寫出組的角色/目的)

category_id?

模塊類別用于將組與Odoo應用程序(一組相關的業務模型)關聯,并將它們轉換為用戶表單中的獨占選擇。

implied_ids?

在此用戶旁邊設置的其他組。這是一種方便的偽繼承關系:可以明確地從用戶中刪除隱含的組,而不必刪除隱含者。

comment?

關于組的附加說明,例如:

訪問權限?

授予 給定一組操作的整個模型的訪問權限。如果沒有訪問權限與用戶(通過其組)在模型上的操作匹配,則用戶無法訪問。

訪問權限是可疊加的,用戶的訪問權限是他們通過所有組獲得的訪問權限的并集,例如,給定一個用戶屬于組A,授予讀取和創建訪問權限,以及一個組B授予更新訪問權限,用戶將擁有創建、讀取和更新的所有三個權限。

class ir.model.access?
name?

組的目的或角色。

model_id?

控制訪問控制列表的模型。

group_id?

授予訪問權限的 res.groups ,一個空的 group_id 表示該 ACL 授予給 每個用戶 (非員工,例如門戶或公共用戶)。

perm_method 屬性在設置時授予相應的 CRUD 訪問權限,它們默認情況下都未設置。

perm_create?
perm_read?
perm_write?

記錄規則?

記錄規則是必須滿足的 條件 ,才能允許操作。記錄規則按記錄逐個評估,遵循訪問權限。

記錄規則是默認允許的:如果訪問權限授予訪問權并且對于用戶的操作和模型沒有適用的規則,則授予訪問權限。

class ir.rule?
name?

規則的描述。

model_id?

適用規則的模型。

groups?

授予或拒絕訪問的 res.groups (或多個)??梢灾付ǘ鄠€組。如果未指定組,則規則是 全局 ,與“組”規則不同(見下文)。

global?

基于 groups 計算,提供了對規則全局狀態(或非狀態)的便捷訪問。

domain_force?

當作為 domain 指定的謂詞,該規則允許選擇的操作,如果域與記錄匹配,則允許,否則禁止。

域是一個 Python表達式 ,可以使用以下變量:

time

Python的 time 模塊。

user

當前用戶,作為單例記錄集。

company_id

當前用戶當前選擇的公司作為單個公司ID(而不是記錄集)。

company_ids

當前用戶可以訪問的所有公司的公司ID列表(不是記錄集),有關詳細信息,請參見: 安全規則 。

perm_method 的語義與 ir.model.access 完全不同:對于規則,它們指定規則適用于哪個操作。如果未選擇操作,則該規則不會針對該操作進行檢查,就像規則不存在一樣。

默認情況下選擇所有操作。

perm_create?
perm_read?
perm_write?

全局規則與組規則?

全局規則和組規則在組合和合并方面存在很大的差異:

  • 全局規則 相交 ,如果兩個全局規則都適用,則 必須滿足才能授予訪問權限,這意味著添加全局規則總是會進一步限制訪問權限。

  • 群組規則 合并 ,如果有兩個群組規則適用,則 任意一個 都可以滿足訪問要求。這意味著添加群組規則可以擴展訪問權限,但不能超出全局規則定義的范圍。

  • 全局規則集和組規則集 相交 ,這意味著添加到給定全局規則集的第一個組規則將限制訪問。

危險

創建多個全局規則是有風險的,因為可能會創建不重疊的規則集,這將刪除所有訪問權限。

字段訪問?

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.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})

將方法設為私有顯然是不夠的,必須小心謹慎地使用它。

繞過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 向量。

很難控制從計算到最終在瀏覽器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;
}

轉義與凈化?

重要

當您混合數據和代碼時,無論數據有多安全,轉義始終是100%必需的。

**轉義 ** 將 TEXT 轉換為 CODE 。每次將 DATA/TEXTCODE 混合使用時(例如生成要在 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
'&lt;R&amp;D&gt;'

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

消毒CODE 轉換為 更安全的CODE (但不一定是 安全的 代碼)。它不適用于 TEXT 。只有在 CODE 不受信任時才需要進行消毒,因為它完全或部分來自某些用戶提供的數據。如果用戶提供的數據是以 TEXT 的形式出現的(例如用戶填寫的表單內容),并且在將其放入 CODE 之前已正確轉義,則消毒是無用的(但仍然可以進行)。但是,如果用戶提供的數據沒有進行轉義,則消毒將 不會 按預期工作。

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

消毒可能會破壞功能,這取決于 CODE 是否預期包含不安全的模式。這就是為什么 fields.Htmltools.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)
'<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()

訪問對象屬性?

如果需要動態檢索或修改記錄的值,則可以使用 getattrsetattr 方法。

# 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和字段值進行額外的驗證。