Bảo mật trong SoOn

Ngoài việc quản lý quyền truy cập theo cách thủ công bằng mã tùy chỉnh, SoOn còn cung cấp hai cơ chế dựa trên dữ liệu chính để quản lý hoặc hạn chế quyền truy cập vào dữ liệu.

Cả hai cơ chế đều được liên kết với những người dùng cụ thể thông qua groups: một người dùng thuộc bất kỳ nhóm nào và các cơ chế bảo mật được liên kết với các nhóm, do đó áp dụng các cơ chế bảo mật cho người dùng.

class res.groups
name

đóng vai trò là thông tin nhận dạng mà người dùng có thể đọc được cho nhóm (nêu rõ vai trò/mục đích của nhóm)

category_id

danh mục mô-đun, dùng để liên kết các nhóm với Ứng dụng SoOn (~một tập hợp các mô hình kinh doanh có liên quan) và chuyển đổi chúng thành một lựa chọn độc quyền trong biểu mẫu người dùng.

implied_ids

Các nhóm khác sẽ được thiết lập cho người dùng cùng với nhóm này. Đây là mối quan hệ giả thừa kế tiện lợi: có thể xóa rõ ràng các nhóm ngụ ý khỏi người dùng mà không cần xóa hàm ẩn.

comment

Ghi chú bổ sung về nhóm, ví dụ:

Quyền truy cập

Cấp quyền truy cập vào toàn bộ mô hình cho một tập hợp hoạt động nhất định. Nếu không có quyền truy cập nào khớp với thao tác trên mô hình cho người dùng (thông qua nhóm của họ) thì người dùng đó sẽ không có quyền truy cập.

Quyền truy cập có tính chất bổ sung, quyền truy cập của người dùng là sự kết hợp của các quyền truy cập mà họ có được thông qua tất cả các nhóm của họ, ví dụ: nếu người dùng thuộc nhóm A cấp quyền truy cập đọc và tạo và nhóm B cấp quyền truy cập cập nhật, người dùng sẽ có cả ba quyền tạo, đọc và cập nhật.

class ir.model.access
name

Mục đích hoặc vai trò của nhóm.

model_id

Mô hình có quyền truy cập vào các điều khiển ACL.

group_id

res.groups mà quyền truy cập được cấp, group_id trống có nghĩa là ACL được cấp cho mọi người dùng (những người không phải là nhân viên, ví dụ: cổng thông tin hoặc người dùng công cộng).

Các thuộc tính perm_method cấp quyền truy cập CRUD tương ứng khi được đặt, tất cả chúng đều không được đặt theo mặc định.

perm_create
perm_read
perm_write

Quy tắc ghi

Quy tắc ghi là điều kiện phải được đáp ứng để một thao tác được phép. Quy tắc bản ghi được đánh giá theo từng bản ghi, tuân theo quyền truy cập.

Quy tắc ghi là cho phép mặc định: nếu quyền truy cập cấp quyền truy cập và không có quy tắc nào áp dụng cho hoạt động và mô hình cho người dùng thì quyền truy cập sẽ được cấp.

class ir.rule
name

Mô tả của quy tắc.

model_id

Mô hình mà quy tắc áp dụng.

groups

res.groups được cấp quyền truy cập (hoặc không). Nhiều nhóm có thể được chỉ định. Nếu không có nhóm nào được chỉ định thì quy tắc là toàn cầu được xử lý khác với quy tắc "nhóm" (xem bên dưới).

global

Được tính toán trên cơ sở groups, cung cấp khả năng truy cập dễ dàng vào trạng thái chung (hoặc không) của quy tắc.

domain_force

Một vị từ được chỉ định dưới dạng domain, quy tắc cho phép các hoạt động được chọn nếu miền khớp với bản ghi và cấm nó nếu không.

Miền là một biểu thức python có thể sử dụng các biến sau:

thời gian

Mô-đun time của Python.

người dùng

Người dùng hiện tại, dưới dạng một tập bản ghi đơn lẻ.

id_công ty

Công ty hiện được chọn của người dùng hiện tại làm id công ty duy nhất (không phải tập bản ghi).

id_công ty

Tất cả các công ty mà người dùng hiện tại có quyền truy cập dưới dạng danh sách id công ty (không phải tập bản ghi), hãy xem Quy tắc bảo mật để biết thêm chi tiết.

perm_method có ngữ nghĩa hoàn toàn khác so với ir.model.access: đối với các quy tắc, chúng chỉ định thao tác nào mà quy tắc áp dụng cho. Nếu một thao tác không được chọn thì quy tắc đó sẽ không được kiểm tra, như thể quy tắc đó không tồn tại.

Tất cả các hoạt động được chọn theo mặc định.

perm_create
perm_read
perm_write

Quy tắc chung so với quy tắc nhóm

Có sự khác biệt lớn giữa các quy tắc chung và quy tắc nhóm trong cách chúng soạn thảo và kết hợp:

  • Quy tắc chung giao nhau, nếu áp dụng hai quy tắc chung thì cả hai phải được đáp ứng để cấp quyền truy cập, điều này có nghĩa là việc thêm các quy tắc chung luôn hạn chế quyền truy cập hơn nữa.

  • Quy tắc nhóm thống nhất, nếu áp dụng hai quy tắc nhóm thì hoặc có thể được thỏa mãn để cấp quyền truy cập. Điều này có nghĩa là việc thêm quy tắc nhóm có thể mở rộng quyền truy cập nhưng không vượt quá giới hạn được xác định bởi quy tắc chung.

  • Các bộ quy tắc chung và nhóm giao nhau, có nghĩa là quy tắc nhóm đầu tiên được thêm vào một bộ quy tắc chung nhất định sẽ hạn chế quyền truy cập.

Nguy hiểm

Việc tạo nhiều quy tắc chung rất rủi ro vì có thể tạo các bộ quy tắc không chồng chéo, điều này sẽ loại bỏ tất cả quyền truy cập.

Truy cập trường

Một ORM Field có thể có thuộc tính groups cung cấp danh sách các nhóm (dưới dạng chuỗi định danh bên ngoài được phân tách bằng dấu phẩy).

Nếu người dùng hiện tại không thuộc một trong các nhóm được liệt kê, anh ta sẽ không có quyền truy cập vào trường:

  • các trường bị hạn chế sẽ tự động bị xóa khỏi chế độ xem được yêu cầu

  • các trường bị hạn chế sẽ bị xóa khỏi các phản hồi fields_get()

  • cố gắng (rõ ràng) đọc từ hoặc ghi vào các trường bị hạn chế dẫn đến lỗi truy cập

Cạm bẫy an ninh

Là một nhà phát triển, điều quan trọng là phải hiểu các cơ chế bảo mật và tránh những lỗi phổ biến dẫn đến mã không an toàn.

Phương pháp công cộng không an toàn

Bất kỳ phương thức công khai nào cũng có thể được thực thi thông qua lệnh gọi RPC với các tham số đã chọn. Các phương thức bắt đầu bằng _ không thể gọi được từ nút hành động hoặc API bên ngoài.

Trên các phương thức công khai, bản ghi mà phương thức được thực thi và các tham số không thể tin cậy được, ACL chỉ được xác minh trong các hoạt động 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})

Làm cho một phương thức trở nên riêng tư rõ ràng là chưa đủ và cần phải cẩn thận để sử dụng nó đúng cách.

Bỏ qua ORM

Bạn không bao giờ nên sử dụng trực tiếp con trỏ cơ sở dữ liệu khi ORM có thể thực hiện điều tương tự! Bằng cách đó, bạn đang bỏ qua tất cả các tính năng ORM, có thể là các hành vi tự động như bản dịch, vô hiệu hóa các trường, hoạt động, quyền truy cập, v.v.

Và rất có thể bạn cũng đang làm cho mã khó đọc hơn và có thể kém an toàn hơn.

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

Tiêm SQL

Phải cẩn thận để không tạo ra các lỗ hổng chèn SQL khi sử dụng các truy vấn SQL thủ công. Lỗ hổng này xuất hiện khi đầu vào của người dùng được lọc không chính xác hoặc được trích dẫn kém, cho phép kẻ tấn công đưa các mệnh đề không mong muốn vào truy vấn SQL (chẳng hạn như phá vỡ các bộ lọc hoặc thực thi các lệnh UPDATE hoặc DELETE).

Cách tốt nhất để đảm bảo an toàn là KHÔNG BAO GIỜ sử dụng phép nối chuỗi Python (+) hoặc nội suy tham số chuỗi (%) để chuyển các biến sang chuỗi truy vấn SQL.

Lý do thứ hai, gần như quan trọng, đó là công việc của lớp trừu tượng cơ sở dữ liệu (psycopg2) là quyết định cách định dạng các tham số truy vấn chứ không phải công việc của bạn! Ví dụ: psycopg2 biết rằng khi bạn chuyển một danh sách các giá trị, nó cần định dạng chúng dưới dạng danh sách được phân tách bằng dấu phẩy, được đặt trong dấu ngoặc đơn!

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

Điều này rất quan trọng, vì vậy hãy cẩn thận khi tái cấu trúc và quan trọng nhất là không sao chép các mẫu này!

Đây là một ví dụ đáng nhớ để giúp bạn nhớ vấn đề là gì (nhưng không sao chép mã vào đó). Trước khi tiếp tục, hãy nhớ đọc tài liệu trực tuyến về pyscopg2 để tìm hiểu cách sử dụng nó đúng cách:

Nội dung trường không thoát

Khi hiển thị nội dung bằng JavaScript và XML, người ta có thể muốn sử dụng t-raw để hiển thị nội dung văn bản đa dạng thức. Nên tránh điều này dưới dạng vectơ XSS thường xuyên.

Rất khó để kiểm soát tính toàn vẹn của dữ liệu từ quá trình tính toán cho đến khi tích hợp cuối cùng vào DOM trình duyệt. Một t-raw được thoát chính xác tại thời điểm giới thiệu có thể không còn an toàn ở lần sửa lỗi hoặc tái cấu trúc tiếp theo.

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>

Đoạn mã trên có thể mang lại cảm giác an toàn vì nội dung tin nhắn được kiểm soát nhưng là một phương pháp không tốt có thể dẫn đến các lỗ hổng bảo mật không mong muốn khi mã này phát triển trong tương lai.

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

Mặc dù định dạng mẫu khác nhau sẽ ngăn chặn những lỗ hổng như vậy.

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

Tạo nội dung an toàn bằng Markup

Hãy xem tài liệu chính thức để biết giải thích, nhưng ưu điểm lớn của Markup là nó có kiểu ghi đè rất phong phú str hoạt động để tự động thoát khỏi các tham số.

Điều này có nghĩa là thật dễ dàng để tạo các đoạn mã html safe bằng cách sử dụng Markup trên một chuỗi ký tự và "định dạng trong" nội dung do người dùng cung cấp (và do đó có khả năng không an toàn):

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

mặc dù đó là một điều rất tốt, nhưng hãy lưu ý rằng đôi khi các hiệu ứng có thể kỳ quặc:

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

Mẹo

Hầu hết các API an toàn nội dung thực sự trả về Markup với tất cả những gì ngụ ý.

Phương thức escape (và bí danh của nó html_escape) biến str thành Markup và thoát khỏi nội dung của nó. Nó sẽ không thoát khỏi nội dung của đối tượng Markup.

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

Khi tạo mã HTML, điều quan trọng là phải tách biệt cấu trúc (thẻ) khỏi nội dung (văn bản).

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

Khi làm việc với các bản dịch, điều đặc biệt quan trọng là tách HTML khỏi văn bản. Các phương thức dịch chấp nhận tham số Markup và sẽ thoát khỏi bản dịch nếu nó nhận được ít nhất một tham số.

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

Chạy trốn và vệ sinh

Quan trọng

Việc thoát luôn là bắt buộc 100% khi bạn kết hợp dữ liệu và mã, bất kể dữ liệu có an toàn đến đâu

Thoát chuyển đổi TEXT thành CODE. Bạn hoàn toàn bắt buộc phải làm điều đó mỗi khi kết hợp DATA/TEXT với CODE (ví dụ: tạo mã HTML hoặc python để được đánh giá bên trong safe_eval), vì CODE luôn yêu cầu TEXT phải được mã hóa. Nó rất quan trọng đối với vấn đề bảo mật nhưng cũng là vấn đề về tính chính xác. Ngay cả khi không có rủi ro bảo mật (vì văn bản được đảm bảo 100% là an toàn hoặc đáng tin cậy), điều này vẫn được yêu cầu (ví dụ: để tránh phá vỡ bố cục trong HTML được tạo).

Việc thoát sẽ không bao giờ phá vỡ bất kỳ tính năng nào, miễn là nhà phát triển xác định được biến nào chứa TEXT và biến nào chứa 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

Khử trùng chuyển đổi CODE thành MÃ AN TOÀN (nhưng mã an toàn không cần thiết). Nó không hoạt động trên TEXT. Việc dọn dẹp chỉ cần thiết khi CODE không đáng tin cậy vì nó có toàn bộ hoặc một phần từ một số dữ liệu do người dùng cung cấp. Nếu dữ liệu do người dùng cung cấp ở dạng TEXT (ví dụ: nội dung từ biểu mẫu do người dùng điền) và nếu dữ liệu đó đã được thoát chính xác trước khi đưa vào CODE thì việc khử trùng là vô ích (nhưng vẫn có thể được làm). Tuy nhiên, nếu dữ liệu do người dùng cung cấp không thoát thì quá trình khử trùng sẽ không hoạt động như mong đợi.

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

Quá trình khử trùng có thể làm hỏng các tính năng, tùy thuộc vào việc CODE có dự kiến chứa các mẫu không an toàn hay không. Đó là lý do tại sao fields.Htmltools.html_sanitize() có các tùy chọn để tinh chỉnh mức độ dọn dẹp cho các kiểu, v.v. Các tùy chọn đó phải được xem xét cẩn thận tùy thuộc vào nguồn gốc của dữ liệu và các tính năng mong muốn. Sự an toàn của quá trình vệ sinh được cân bằng với sự cố vỡ của quá trình vệ sinh: việc vệ sinh càng an toàn thì càng có nhiều khả năng làm hỏng mọi thứ.

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

Đánh giá nội dung

Một số có thể muốn `` đánh giá`` để phân tích nội dung do người dùng cung cấp. Nên tránh sử dụng eval bằng mọi giá. Một phương thức an toàn hơn, được đóng hộp cát, safe_eval có thể được sử dụng thay thế nhưng vẫn mang lại khả năng to lớn cho người dùng đang chạy nó và phải được dành riêng cho những người dùng có đặc quyền đáng tin cậy vì nó phá vỡ rào cản giữa mã và dữ liệu.

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

Phân tích nội dung không cần eval

Ngôn ngữ

Loại dữ liệu

Trình phân tích cú pháp phù hợp

Python

int, float, v.v.

int(), float()

Javascript

int, float, v.v.

phân tích cú phápInt(), phân tích cú phápFloat()

Python

mệnh lệnh

json.loads(), ast.literal_eval()

Javascript

đối tượng, danh sách, v.v.

JSON.parse()

Truy cập thuộc tính đối tượng

Nếu các giá trị của một bản ghi cần được truy xuất hoặc sửa đổi một cách linh hoạt, người ta có thể muốn sử dụng các phương thức 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)

Tuy nhiên, mã này không an toàn vì nó cho phép truy cập bất kỳ thuộc tính nào của bản ghi, bao gồm các thuộc tính hoặc phương thức riêng tư.

__getitem__ của một tập bản ghi đã được xác định và việc truy cập giá trị trường động có thể dễ dàng đạt được một cách an toàn:

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

Phương pháp trên rõ ràng vẫn còn quá lạc quan và cần phải thực hiện xác minh bổ sung về id bản ghi và giá trị trường.