Biztonság az Odoo-ban

Az egyedi kód használatával történő kézi hozzáférés-kezelésen kívül az Odoo két fő adatvezérelt mechanizmust biztosít az adatokhoz való hozzáférés kezelésére vagy korlátozására.

Mindkét mechanizmus konkrét felhasználókhoz van kapcsolva csoportokon keresztül: egy felhasználó bármennyi csoporthoz tartozhat, és a biztonsági mechanizmusok csoportokhoz vannak társítva, így a biztonsági mechanizmusok a felhasználókra alkalmazhatók.

class res.groups
name

szolgál a csoport felhasználó által olvasható azonosítására (kifejti a csoport szerepét / célját)

category_id

A modulkategória arra szolgál, hogy csoportokat társítson egy Odoo alkalmazáshoz (~egy kapcsolódó üzleti modellek halmaza), és exkluzív választássá alakítsa őket a felhasználói űrlapon.

implied_ids

Egyéb csoportok, amelyeket a felhasználónál ezzel együtt kell beállítani. Ez egy kényelmi álinheritancia kapcsolat: lehetséges kifejezetten eltávolítani a felhasználótól az implikált csoportokat anélkül, hogy az implikálót eltávolítanánk.

comment

További megjegyzések a csoportról pl.

Hozzáférési jogok

Teljes hozzáférést biztosít egy modellhez egy adott műveletkészlethez. Ha egy felhasználó (a csoportján keresztül) egy modell egy műveletéhez nem rendelkezik hozzáférési jogokkal, akkor a felhasználó nem fér hozzá.

A hozzáférési jogok összeadódnak, a felhasználó hozzáférései azoknak a hozzáféréseknek az uniója, amelyeket az összes csoportján keresztül kap, például ha egy felhasználó az A csoport tagja, amely olvasási és létrehozási hozzáférést biztosít, és a B csoport tagja, amely frissítési hozzáférést biztosít, akkor a felhasználó mindhárom hozzáféréssel rendelkezik: létrehozás, olvasás és frissítés.

class ir.model.access
name

A csoport célja vagy szerepe.

model_id

A modell, amelynek hozzáférését az ACL szabályozza.

group_id

Az a res.groups, amelyhez a hozzáférések biztosítottak, egy üres group_id azt jelenti, hogy az ACL minden felhasználónak biztosított (nem alkalmazottak, például portál vagy nyilvános felhasználók).

A perm_method attribútumok a megfelelő CRUD hozzáférést biztosítják, ha be vannak állítva, alapértelmezés szerint mind nincsenek beállítva.

perm_create
perm_read
perm_write

Rekordszabályok

A rekordszabályok feltételek, amelyeket teljesíteni kell ahhoz, hogy egy művelet engedélyezett legyen. A rekordszabályokat rekordonként értékelik, a hozzáférési jogokat követve.

A rekordszabályok alapértelmezés szerint engedélyeznek: ha a hozzáférési jogok hozzáférést biztosítanak, és nincs szabály, amely vonatkozna a műveletre és a modellre a felhasználó számára, akkor a hozzáférés engedélyezett.

class ir.rule
name

A szabály leírása.

model_id

A modell, amelyre a szabály vonatkozik.

groups

The res.groups to which access is granted (or not). Multiple groups can be specified. If no group is specified, the rule is global which is treated differently than „group” rules (see below).

global

Az groups alapján számítva, könnyű hozzáférést biztosít a szabály globális státuszához (vagy annak hiányához).

domain_force

Egy domain-ként megadott predikátum, a szabály engedélyezi a kiválasztott műveleteket, ha a domain megfelel a rekordnak, és megtiltja, ha nem.

A domain egy python kifejezés, amely a következő változókat használhatja:

time

Python time modulja.

user

Az aktuális felhasználó, mint egyetlen rekordhalmaz.

company_id

Az aktuális felhasználó jelenleg kiválasztott vállalata, mint egyetlen vállalat azonosító (nem rekordhalmaz).

company_ids

Az összes vállalat, amelyhez a jelenlegi felhasználó hozzáfér, mint vállalat azonosítók listája (nem rekordhalmaz), további részletekért lásd Biztonsági szabályok.

A perm_method teljesen más szemantikával rendelkezik, mint a ir.model.access: a szabályok esetében azt határozzák meg, hogy mely műveletre vonatkozik a szabály. Ha egy művelet nincs kiválasztva, akkor a szabályt nem ellenőrzik rá, mintha a szabály nem létezne.

Alapértelmezés szerint minden művelet ki van választva.

perm_create
perm_read
perm_write

Globális szabályok versus csoport szabályok

Nagy különbség van a globális és a csoport szabályok között abban, hogyan épülnek fel és kombinálódnak:

  • A globális szabályok metsződnek, ha két globális szabály érvényes, akkor mindkettőnek teljesülnie kell ahhoz, hogy a hozzáférés engedélyezett legyen, ez azt jelenti, hogy a globális szabályok hozzáadása mindig tovább korlátozza a hozzáférést.

  • A csoport szabályok egyesülnek, ha két csoport szabály érvényes, akkor bármelyik teljesülhet ahhoz, hogy a hozzáférés engedélyezett legyen. Ez azt jelenti, hogy a csoport szabályok hozzáadása bővítheti a hozzáférést, de nem a globális szabályok által meghatározott határokon túl.

  • A globális és csoport szabályhalmazok metsződnek, ami azt jelenti, hogy az első csoport szabály hozzáadása egy adott globális szabályhalmazhoz korlátozni fogja a hozzáférést.

Veszély

Több globális szabály létrehozása kockázatos, mivel lehetséges nem átfedő szabályhalmazokat létrehozni, amelyek minden hozzáférést eltávolítanak.

Mező hozzáférés

Egy ORM Field rendelkezhet egy groups attribútummal, amely csoportok listáját adja meg (vesszővel elválasztott külső azonosítók formájában).

Ha az aktuális felhasználó nincs a felsorolt csoportok egyikében sem, nem fog hozzáférni a mezőhöz:

  • a korlátozott mezők automatikusan eltávolításra kerülnek a kért nézetekből

  • a korlátozott mezők eltávolításra kerülnek a fields_get() válaszokból

  • a korlátozott mezőkből való (kifejezett) olvasási vagy írási kísérletek hozzáférési hibát eredményeznek

Biztonsági buktatók

Fejlesztőként fontos megérteni a biztonsági mechanizmusokat és elkerülni a gyakori hibákat, amelyek nem biztonságos kódhoz vezetnek.

Nem biztonságos nyilvános metódusok

Bármely nyilvános metódus végrehajtható egy RPC hívás segítségével a választott paraméterekkel. Azok a metódusok, amelyek _-val kezdődnek, nem hívhatók meg egy művelet gombból vagy külső API-ból.

Nyilvános metódusok esetén a rekord, amelyen a metódus végrehajtásra kerül, és a paraméterek nem megbízhatóak, mivel az ACL csak a CRUD műveletek során kerül ellenőrzésre.

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

Egy metódus priváttá tétele nyilvánvalóan nem elegendő, és gondosan kell eljárni annak megfelelő használatakor.

Az ORM megkerülése

Soha ne használja közvetlenül az adatbázis kurzort, ha az ORM ugyanazt a feladatot el tudja végezni! Ezzel megkerüli az összes ORM funkciót, esetleg az automatikus viselkedéseket, mint például a fordítások, mezők érvénytelenítése, active, hozzáférési jogok stb.

És valószínű, hogy ezzel a kódot is nehezebben olvashatóvá és valószínűleg kevésbé biztonságossá teszi.

# 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 befecskendezések

Ügyelni kell arra, hogy ne vezessünk be SQL befecskendezési sebezhetőségeket manuális SQL lekérdezések használatakor. A sebezhetőség akkor áll fenn, ha a felhasználói bemenetet vagy helytelenül szűrik, vagy rosszul idézik, lehetővé téve a támadónak, hogy nem kívánt záradékokat vezessen be egy SQL lekérdezésbe (például szűrők megkerülése vagy UPDATE vagy DELETE parancsok végrehajtása).

A legjobb módja annak, hogy biztonságban legyünk, ha soha, SOHA nem használunk Python string összefűzést (+) vagy string paraméter interpolációt (%) változók SQL lekérdezés stringbe való átadására.

A második ok, amely szinte ugyanolyan fontos, hogy az adatbázis absztrakciós réteg (psycopg2) feladata eldönteni, hogyan formázza a lekérdezési paramétereket, nem az Ön feladata! Például a psycopg2 tudja, hogy amikor egy értéklistát ad meg, azt vesszővel elválasztott listaként kell formázni, zárójelbe foglalva!

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

Ez nagyon fontos, ezért kérjük, legyen óvatos a refaktorálás során is, és ami a legfontosabb, ne másolja ezeket a mintákat!

Íme egy emlékezetes példa, amely segít emlékezni arra, hogy miről szól a probléma (de ne másolja a kódot onnan). Mielőtt folytatná, kérjük, olvassa el a psycopg2 online dokumentációját, hogy megtanulja, hogyan használja helyesen:

Nem escape-elt mezőtartalom

Amikor JavaScript és XML használatával jelenít meg tartalmat, csábító lehet egy t-raw használata a gazdag szöveges tartalom megjelenítésére. Ezt kerülni kell, mivel gyakori XSS vektor.

Nagyon nehéz ellenőrizni az adatok integritását a számítástól a végső integrációig a böngésző DOM-ban. Egy t-raw, amely a bevezetéskor helyesen van escape-elve, a következő hibajavítás vagy refaktorálás során már nem biztos, hogy biztonságos.

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>

A fenti kód biztonságosnak tűnhet, mivel az üzenet tartalma ellenőrzött, de rossz gyakorlat, amely váratlan biztonsági sebezhetőségekhez vezethet, ha ez a kód a jövőben fejlődik.

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

A sablon eltérő formázása megakadályozná az ilyen sebezhetőségeket.

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

Biztonságos tartalom létrehozása a Markup használatával

Lásd a hivatalos dokumentációt a magyarázatokért, de a Markup nagy előnye, hogy egy nagyon gazdag típus, amely felülírja a str műveleteket, hogy automatikusan escape-elje a paramétereket.

This means that it’s easy to create safe html snippets by using Markup on a string literal and „formatting in” user-provided (and thus potentially unsafe) content:

>>> 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;')

bár ez nagyon jó dolog, vegye figyelembe, hogy a hatások néha furcsák lehetnek:

>>> 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;')

Javaslat

A legtöbb tartalom-biztonságos API valójában egy Markup-ot ad vissza, minden ezzel járó következménnyel.

A escape metódus (és annak aliasza html_escape) egy str-t Markup-ká alakít és escape-eli annak tartalmát. Nem fogja escape-elni egy Markup objektum tartalmát.

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 kód generálásakor fontos a struktúra (címkék) és a tartalom (szöveg) szétválasztása.

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

Fordításokkal való munka során különösen fontos az HTML és a szöveg szétválasztása. A fordítási metódusok elfogadnak egy Markup paramétert, és escape-elik a fordítást, ha legalább egyet kapnak.

>>> 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:[email protected]>[email protected]</a>&gt;')

Escape-elés vs Tisztítás

Fontos

Az escape-elés mindig 100%-ban kötelező, amikor adatot és kódot kever, függetlenül attól, mennyire biztonságos az adat

Escape-elés átalakítja a SZÖVEGET KÓDDÁ. Abszolút kötelező ezt megtenni minden alkalommal, amikor ADATOT/SZÖVEGET kever KÓDDAL (pl. HTML vagy python kód generálása, amelyet egy safe_eval-ben értékelnek ki), mert a KÓD mindig megköveteli, hogy a SZÖVEG kódolva legyen. Ez kritikus a biztonság szempontjából, de a helyesség kérdése is. Még akkor is, ha nincs biztonsági kockázat (mert a szöveg 100%-ban garantáltan biztonságos vagy megbízható), még mindig szükséges (pl. hogy elkerüljük a generált HTML elrendezésének megszakítását).

Az escape használata soha nem fog semmilyen funkciót megszakítani, amennyiben a fejlesztő azonosítja, hogy melyik változó tartalmaz SZÖVEGET és melyik tartalmaz KÓDOT.

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

Tisztítás a KÓDOT BIZTONSÁGOSABB KÓDDÁ alakítja (de nem feltétlenül biztonságos kóddá). Nem működik SZÖVEGEN. A tisztítás csak akkor szükséges, ha a KÓD nem megbízható, mert teljes egészében vagy részben valamilyen felhasználó által megadott adatból származik. Ha a felhasználó által megadott adat SZÖVEG formájában van (pl. egy felhasználó által kitöltött űrlap tartalma), és ha ezeket az adatokat helyesen escape-elték, mielőtt a KÓDBA helyezték volna, akkor a tisztítás haszontalan (de még mindig elvégezhető). Ha azonban a felhasználó által megadott adat nem volt escape-elve, akkor a tisztítás nem fog a vártnak megfelelően működni.

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

A tisztítás megszakíthatja a funkciókat, attól függően, hogy a KÓD várhatóan tartalmaz-e nem biztonságos mintákat. Ezért van fields.Html és tools.html_sanitize() opciók a stílusok stb. tisztítási szintjének finomhangolására. Ezeket az opciókat gondosan mérlegelni kell attól függően, hogy honnan származik az adat, és milyen funkciókat kívánunk elérni. A tisztítás biztonsága és a tisztítás okozta megszakítások közötti egyensúly: minél biztonságosabb a tisztítás, annál valószínűbb, hogy megszakít dolgokat.

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

Tartalom értékelése

Néhányan szeretnék az eval-t használni a felhasználó által megadott tartalom elemzésére. Az eval használatát minden áron kerülni kell. Egy biztonságosabb, sandboxolt módszer, safe_eval használható helyette, de még mindig hatalmas képességeket ad a felhasználónak, aki futtatja, és csak megbízható, kiváltságos felhasználók számára kell fenntartani, mivel megszakítja a kód és az adatok közötti határt.

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

A tartalom elemzéséhez nincs szükség eval-ra

Nyelv

Adattípus

Megfelelő elemző

Python

int, float, stb.

int(), float()

Javascript

int, float, stb.

parseInt(), parseFloat()

Python

dict

json.loads(), ast.literal_eval()

Javascript

object, list, stb.

JSON.parse()

Objektum attribútumainak elérése

Ha egy rekord értékeit dinamikusan kell lekérdezni vagy módosítani, érdemes lehet a getattr és setattr metódusokat használni.

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

Ez a kód azonban nem biztonságos, mivel lehetővé teszi a rekord bármely tulajdonságának elérését, beleértve a privát attribútumokat vagy metódusokat is.

A rekordhalmaz __getitem__ metódusa definiálva lett, és egy dinamikus mezőérték biztonságos elérése könnyen megvalósítható:

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

A fenti módszer nyilvánvalóan még mindig túl optimista, és további ellenőrzéseket kell végezni a rekord azonosítóján és a mező értékén.