Odoo でのセキュリティ

カスタムコードを使用して手動でアクセスを管理することは別として、Odoo はデータにアクセスを管理または制限する2つの主要なデータ駆動メカニズムを提供します。

どちらのメカニズムも*groups*を通じて特定のユーザーにリンクされます。ユーザーは任意の数のグループに属します。 セキュリティメカニズムはグループに関連しておりユーザーにセキュリティメカニズムを適用しています

class res.groups
name

は、グループのユーザー読み取り可能な識別として機能します (グループの役割/目的を綴る)

category_id

モジュールカテゴリ、 グループをOdoo App(~関連するビジネスモデルのセット)に関連付け、ユーザーフォームでの排他的な選択に変換するのに役立ちます。

implied_ids

このグループと一緒にユーザーに設定する他のグループ これは便利な擬似継承関係です。暗黙のグループをユーザーから明示的に削除することができます。

comment

グループ上の追加のメモ例:

アクセス権限

*与えられた一連の操作に対するモデル全体へのアクセス権限を付与します。 アクセス権がユーザーのモデル上の操作と一致しない場合 (グループを通じて)、ユーザーはアクセス権を持っていません。

アクセス権は付加的であり、ユーザーのアクセスはすべてのグループを通過するアクセスの組合です。例えば、 グループ A の一部であるユーザが読み取りおよび作成を許可し、グループ B が更新アクセスを許可している場合。 ユーザーは、作成、読み取り、および更新の3つすべてを持っています。

class ir.model.access
name

グループの目的または役割。

model_id

ACL コントロールにアクセスするモデル。

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 。複数のグループを指定できます。 グループが指定されていない場合、ルールは "group" 規則とは異なる扱いの global になります (下記参照)。

global

groups に基づいて計算されると、ルールのグローバルステータス(またはそうでない)に簡単にアクセスできます。

domain_force

:ref:`domain <reference/orm/domains>`として指定された述語。このルールは、ドメインがレコードと一致する場合に選択された操作を許可し、そうでない場合には禁止します。

ドメインは python 式 で、以下の変数を使用できます。

time

Python's time module.

user

シングルトンレコードセットとして現在のユーザー。

company_id

現在ユーザーが企業IDとして選択している会社 (レコードセットではありません) 。

company_ids

現在のユーザーが会社ID(レコードセットではない)のリストとしてアクセスできるすべての企業は、詳細については :ref:`howto/company/security`を参照してください。

:samp:`perm_{method}`は、 :class:`ir.model.access`と完全に異なるセマンティクスを持っています。ルールでは、ルールが for に適用されるオペレーションを指定します。 操作が選択されていない場合、ルールが存在しなかったかのようにルールはチェックされません。

すべての操作はデフォルトで選択されています。

perm_create
perm_read
perm_write

グローバルルールとグループルール

グローバルルールとグループ ルールの構成方法と組み合わせ方には大きな違いがあります。

  • グローバルルール intersect、2つのグローバルルールが適用される場合、アクセス許可のために*両方*が満たされなければなりません。 つまり、グローバルルールを追加することは、常にアクセスを制限するということです。

  • グループルール unify、2つのグループルールが適用された場合、アクセス許可のために*どちらか*を満たすことができます。 つまり、グループ ルールを追加すると、アクセスが拡張できますが、グローバル ルールで定義された範囲を超えることはできません。

  • グローバルルールセットとグループルールセットは intersect で、与えられたグローバルルールセットに最初に追加されるグループルールはアクセスを制限します。

危険

複数のグローバルルールを作成することは、重複しないルールセットを作成することが可能であるため、すべてのアクセスを削除します。

フィールドアクセス

ORMの Field`は、グループのリスト( :term:`外部識別子`のカンマ区切り文字列)を提供する``groups` 属性を持つことができます。

現在のユーザーがリストされているグループのいずれかにいない場合、彼はフィールドにアクセスできません:

  • 制限されたフィールドは要求されたビューから自動的に削除されます

  • :meth:`~odoo.models.Model.fields_get`応答から制限フィールドが削除されます

  • 制限されたフィールドからの(明示的に)読み込みまたは書き込みを試みると、アクセスエラーが発生します

セキュリティピットフォールズ

開発者として、セキュリティの仕組みを理解し、安全でないコードにつながるよくある間違いを避けることが重要です。

安全でない公開メソッド

任意のpublicメソッドは、選択したパラメータを使用して RPCコール <api/external_api/calling_methods>`を介して実行できます。 ``_` で始まるメソッドはアクションボタンや外部 API からは呼び出せません。

publicメソッドでは、メソッドが実行され、パラメータは信頼できません。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クエリ文字列に変数を渡すために使用しません。

2つ目の理由はほぼ同じくらい重要です クエリパラメータをフォーマットする方法を決めるのは、データベース抽象化レイヤー(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;
}

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.

これは、 :class:`~markupsafeを使用して、safe html スニペットを簡単に作成することを意味します。 文字列リテラルで「arkup」、ユーザーが提供する「フォーマット」(そして安全でない可能性があります)の内容で:

>>> Markup('<em>Hello</em> ') + '<foo>'
Markup('<em>Hello</em> &lt;foo&gt;')
>>> Markup('<em>Hello</em> %s') % '<foo>'
Markup('<em>Hello</em> &lt;foo&gt;')

とても良いことですが時に奇妙なことになることがあります

>>> Markup('<a>').replace('>', 'x')
Markup('<a>')
>>> Markup('<a>').replace(Markup('>'), 'x')
Markup('<ax')
>>> Markup('<a&gt;').replace('>', 'x')
Markup('<ax')
>>> Markup('<a&gt;').replace('>', '&')
Markup('<a&amp;')

ちなみに

ほとんどのコンテンツセーフ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("&lt;R&amp;D&gt;")
>>> escape(record.get_name(True))
Markup("<strong>&lt;R&amp;D&gt;</strong>")  # HTML is kept

HTML コードを生成する場合、構造 (タグ) をコンテンツ(テキスト)から分離することが重要です。

>>> Markup("<p>") + "Hello <R&D>" + Markup("</p>")
Markup('<p>Hello &lt;R&amp;D&gt;</p>')
>>> Markup("%s <br/> %s") % ("<R&D>", Markup("<p>Hello</p>"))
Markup('&lt;R&amp;D&gt; <br/> <p>Hello</p>')
>>> escape("<R&D>")
Markup('&lt;R&amp;D&gt;')
>>> _("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 &lt;R&amp;D&gt;: <ul><li>First &lt;R&amp;D&gt; 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をテキストから分離することが特に重要です。 翻訳メソッドは、 :class:`~markupsafe.Markup`パラメータを受け取り、少なくとも1つのパラメータを受け取った場合、翻訳をエスケープします。

>>> Markup("<p>%s</p>") % _("Hello <R&D>")
Markup('<p>Bonjour &lt;R&amp;D&gt;</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 &lt;<a href=mailto:george@abitbol.example>george@abitbol.example</a>&gt;')

脱出対サニタイズ法

重要

データとコードを混在させる際には、データがどれほど安全であっても常にエスケープが100%必須です

EscapingTEXTCODE に変換します。DATA/TEXTCODE を組み合わせるたびに必ずそれを行う必要があります。 safe_eval の中で評価される HTML や python コードを生成します。なぜなら、 CODE は常に TEXT をエンコードする必要があるからです。 それはセキュリティにとって重要ですが、正しさの問題でもあります。 セキュリティ上のリスクがない場合でも(テキストが100%安全または信頼されることを保証しているため)、依然として必要です(e. をクリックします。

開発者がどの変数に 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('&lt;R&amp;D&gt;')

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

SanitizingCODESAFER CODE に変換します(ただし、安全な*コードは必要ありません)。*TEXT では動作しません。 サニタイズは、CODE*が信頼されていない場合にのみ必要となります。 ユーザーが提供するデータが *TEXT (e. をクリックします。ユーザーによって入力されたフォームの内容)、データが正しくエスケープされている場合、CODE に入力されます。 そしてサニタイズは役に立たない(でもまだできます)。 しかし、ユーザーが提供するデータが**エスケープされていない**場合、サニタイズは期待通りに動作しません。

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

# Sanitizing *after* escaping is OK!
>>> html_sanitize(code)
Markup('<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)
Markup('<p>Important Information</p>')

コンテンツの評価

ユーザーが提供するコンテンツを解析するために eval を使用したい人もいるかもしれません。eval を使用することは、すべてのコストで避ける必要があります。より安全な、サンドボックス、メソッド tools。 afe_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, etc.

int(), float()

Javascript

int, float, etc.

parseInt(), parseFloat()

Python

dict

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とフィールド値に関する追加の検証を行う必要があります。