Tôi bị Odoo từ chối ... phát hành CVE

Một lô hổng bảo mật mà Odoo đã vá cho SaaS nhưng từ chối phát hành CVE cho open source. Chuyện quái gì vậy bros ??

Tôi bị Odoo từ chối ... phát hành CVE

Bạn đã bao giờ report một lỗ hổng bảo mật, rồi nhận được câu trả lời "À, cái đó chúng tôi biết rồi, đã vá rồi - nhưng chỉ cho khách trả tiền thôi" chưa?

OK, But ... Chuyện quái gì vậy bros ??

Đợt này Odoo quảng cáo rất nhiều trên kênh youtube của Vui Vẻ nên họ đã thành công đưa cái tên của họ vào đầu tôi.
Hôm nay, chiều ngày 18/03/2026, Một buổi chiều WFH như mọi ngày, sau khi họp xong 200 cuộc họp thì tôi quyết định tìm ra cách bypass sandbox safe_eval của Odoo để xả stress.

Tôi tìm ra mộ lỗ hổng nhỏ, và bạn có thể đọc được mật khẩu master, thông tin SMTP, credential PostgreSQL, biến môi trường hệ thống - về cơ bản là toàn bộ config server. Tôi report cho Odoo Security Team ngay trong ngày. Phản hồi của họ khiến tôi cảm thấy họ làm việc như "Ép Bê Tông" vậy (jk, em yêu FPT ❤️!): họ đã biết lỗ hổng này từ lâu, đã vá cho nền tảng SaaS trả phí từ nhiều tháng trước, nhưng từ chối phát hành CVE và chưa phát hành bản vá cho phiên bản open source.

Bài viết này không chỉ về kỹ thuật. Đây là than thở của tôi về cách big tech đối xử với cộng đồng open source - chính cộng đồng đã giúp họ xây dựng sản phẩm từ những ngày đầu.

alt text

TL;DR

  • Lỗ hổng format string bypass trong safe_eval cho phép admin đọc toàn bộ server config
  • Odoo đã biết và vá cho SaaS trả tiền từ nhiều tháng trước
  • Không phát hành CVE, không thông báo cho cộng đồng open source
  • Hàng chục nghìn Odoo self-hosted instance vẫn đang bị ảnh hưởng
  • Bài viết cung cấp workaround để tự vá

Lỗ hổng: Một dòng format string phá vỡ sandbox

safe_eval là gì và tại sao nó quan trọng?

Odoo cho phép admin viết code Python trong "Server Actions" để tự động hóa quy trình nghiệp vụ. Nghe tiện lợi, nhưng cho phép chạy Python tùy ý trên server thì rõ ràng là nguy hiểm. Đó là lý do safe_eval tồn tại - nó là sandbox được thiết kế để chặn các thao tác nguy hiểm như đọc file, chạy lệnh hệ thống, hay import module tùy ý.

Một trong những lớp bảo vệ then chốt của sandbox là hàm assert_no_dunder_name(). Hàm này quét bytecode để chặn mọi truy cập vào thuộc tính "dunder" (double underscore) như __class__, __init__, __globals__ - những cánh cửa dẫn thẳng vào nội bộ Python runtime. Nếu bạn từng chơi Python sandbox escape CTF, bạn sẽ biết đây chính là con đường kinh điển để "vượt ngục".

Cách bypass - đơn giản đến bất ngờ

Vấn đề nằm ở chỗ assert_no_dunder_name() chỉ quét code_obj.co_names - danh sách tên thuộc tính được tham chiếu qua lệnh LOAD_ATTR trong bytecode. Nhưng khi bạn viết một format string như thế này:

"{0.__class__.__init__.__globals__[tools].config[admin_passwd]}".format(env.cr)

Các tên __class__, __init__, __globals__ nằm trong co_consts (hằng số chuỗi), không phải co_names. Toàn bộ việc phân giải thuộc tính diễn ra bên trong C code của str.format(), hoàn toàn ngoài tầm với của sandbox. Nói cách khác, sandbox đang canh cửa trước rất kỹ, trong khi format string đi vào bằng cửa sổ.

Figure 1: Format String Bypass vs Direct Access - cùng mục tiêu, hai kết quả khác nhau

Bị chặn vs. Bypass được

Để thấy rõ sự khác biệt:

# BỊ CHẶN - NameError: "Access to forbidden name '__class__'"
x = env.cr.__class__.__init__.__globals__["os"]

# BYPASS ĐƯỢC - trả về giá trị thật
"{0.__class__.__init__.__globals__[tools].config[admin_passwd]}".format(env.cr)
# -> $pbkdf2-sha512$500000$zPkf4zyHUOr9HyMEwNh7jw$0ppVs5fy...

Cùng một mục tiêu, cùng một chain thuộc tính, nhưng cách viết khác nhau cho kết quả hoàn toàn trái ngược. Sandbox chặn được cách đầu tiên vì __class__ xuất hiện trong co_names. Cách thứ hai lọt qua vì format string giấu mọi thứ trong co_consts.

Dữ liệu trích xuất được

Và đây là những gì tôi đọc được chỉ với một dòng format string:

Thông tin Giá trị mẫu
Hash mật khẩu master (admin_passwd) $pbkdf2-sha512$500000$...
SMTP server (IP nội bộ Docker) 172.17.0.1
Tài khoản PostgreSQL odoo
Đường dẫn file mật khẩu PG /home/odoo/.pgpass
Thư mục dữ liệu /data/build/datadir
Đường dẫn addons ['/data/build/odoo/addons', ...]
Hostname container 684df27ddd79
Biến môi trường (HOME, PATH, ...) /home/odoo
Cấu hình nội bộ safe_eval _UNSAFE_ATTRIBUTES, _ALLOWED_MODULES
Policy xử lý unsafe call log (chỉ ghi log, không chặn)

Mật khẩu master, thông tin database, topology mạng nội bộ - tất cả từ một Server Action mà bất kỳ admin nào cũng có thể tạo.

Xác minh: SaaS đã vá, Open Source thì chưa

Tôi không muốn chỉ dựa vào lời Odoo, nên đã kiểm tra payload trên cả hai môi trường.

Odoo Runbot (code open source từ GitHub) - chạy trên runbot133.odoo.com:

Input: "{0.__class__.__init__.__globals__[tools].config[admin_passwd]}".format(env.cr)
Output: $pbkdf2-sha512$500000$zPkf4zyHUOr9HyMEwNh7jw$0ppVs5fy...
-> KHAI THAC THANH CONG

Odoo Demo (SaaS, đã vá) - chạy trên demo5.odoo.com:

Input: "{0.__class__.__init__.__globals__[tools].config[admin_passwd]}".format(env.cr)
Output: {0.__class__.__init__.__globals__[tools].config[admin_passwd]}
-> Trả về chuỗi nguyên bản, KHÔNG phân giải thuộc tính
-> ĐÃ ĐƯỢC VÁ

Bản vá hoạt động bằng cách chặn str.format() phân giải các thuộc tính dunder - trên phiên bản đã vá, format string trả về đúng chuỗi ký tự ban đầu thay vì traverse qua object chain. Cùng một codebase, cùng một vendor, nhưng hai cấp độ bảo vệ khác nhau tùy thuộc vào việc bạn có trả tiền hay không.

Phản hồi từ Odoo - "Accepted Risk"

Tôi nhận được phản hồi trong cùng ngày report, phải công nhận là khá nhanh. Nhưng nội dung thì... Trích nguyên văn:

"We are already aware of this exploit using str.format. It was found internally by Security developers in our company."

"It is patched on our multi-tenant cloud platforms, such as Odoo Online."

"For non multi-tenant platforms, or docker-like containerized servers, we consider the vulnerability to be an accepted risk."

"We plan to release in the near future the patch [...] We were just waiting for it to be battle-tested for months as the patch is quite complicated."

Để tôi dịch tóm gọn cho rõ: Odoo đã biết lỗ hổng từ trước. Họ đã vá cho nền tảng SaaS trả phí. Còn bản open source? Họ coi đó là "rủi ro chấp nhận được." Bản vá sẽ được phát hành "trong tương lai gần" nhưng không có timeline cụ thể. Và quan trọng nhất - không phát hành CVE.

Figure 2: The Patching Gap - SaaS được bảo vệ từ nhiều tháng, Open Source vẫn vulnerable

Vấn đề thực sự: Không phải kỹ thuật

Bản vá bảo mật không nên là đặc quyền trả tiền

Odoo đã xử lý lỗ hổng này theo mô hình hai tầng, và bảng so sánh dưới đây nói lên tất cả:

Đối tượng Trạng thái Được thông báo?
Khách hàng Odoo Online (SaaS) Đã vá từ nhiều tháng trước Không cần - tự động vá
Người dùng open source Chưa vá Không - không CVE, không advisory

Hàng chục nghìn tổ chức trên thế giới đang chạy Odoo self-hosted - doanh nghiệp, trường học, tổ chức chính phủ, bệnh viện. Tất cả đều không biết rằng bất kỳ admin nào trong hệ thống của họ có thể đọc mật khẩu master và toàn bộ thông tin hạ tầng server. Không ai nói cho họ biết, vì không có CVE, không có advisory, không có gì cả.

"Admin đã có quyền cao nhất" - nghe có lý, nhưng sai

Odoo cho rằng admin đã là người được tin tưởng nên việc đọc config server không phải vấn đề. Lập luận này nghe qua thì có vẻ hợp lý, nhưng nó bỏ qua rất nhiều tình huống thực tế mà bất kỳ ai vận hành Odoo production đều gặp phải.

Nghĩ đến các nhà cung cấp hosting chạy nhiều instance Odoo trên cùng server - admin của khách hàng A không nên đọc được mật khẩu master dùng chung cho tất cả instance, hay thông tin SMTP của nhà cung cấp. Nghĩ đến các tổ chức có nhiều admin với mức độ tin tưởng khác nhau - trưởng phòng được cấp quyền admin để tạo automated action, nhưng điều đó không có nghĩa họ nên thấy hash mật khẩu master hay IP nội bộ Docker.

Và đây là điểm mâu thuẫn lớn nhất: chính sự tồn tại của admin_passwd như một ranh giới bảo mật riêng biệt (mật khẩu để tạo, xóa, backup database) chứng minh rằng Odoo đã thiết kế hệ thống với giả định admin không nên truy cập được mọi thứ. Tương tự, chính sự tồn tại của assert_no_dunder_name() chứng minh Odoo có ý định giới hạn những gì admin có thể truy cập trong sandbox. Format string bypass phá vỡ ranh giới bảo mật mà chính Odoo đã thiết kế - rồi Odoo quay lại nói "ranh giới đó không quan trọng."

Không CVE = Lỗ hổng "không tồn tại"

CVE (Common Vulnerabilities and Exposures) không chỉ là một mã số. Nó là cách toàn bộ hệ sinh thái bảo mật theo dõi và ưu tiên các lỗ hổng. Khi một lỗ hổng không có CVE, nó gần như vô hình: công cụ quét bảo mật không phát hiện được, đội ngũ IT không biết để đánh giá rủi ro, không có áp lực cập nhật, và trong mắt compliance frameworks thì lỗ hổng này đơn giản là "không tồn tại."

Điều đáng nói là Odoo hoàn toàn có khả năng phát hành CVE - họ là CNA (CVE Numbering Authority). Họ đã làm điều này cho CVE-2021-44476, một lỗ hổng safe_eval sandbox escape khác có mức độ nghiêm trọng tương đương. Việc không phát hành CVE cho lỗ hổng này là lựa chọn có chủ đích, không phải thiếu khả năng.

"Battle-testing" hay trì hoãn?

Odoo nói bản vá "phức tạp" và cần "battle-test nhiều tháng." Tôi hiểu rằng bản vá bảo mật phức tạp cần thời gian kiểm chứng - đó là thực tế kỹ thuật hoàn toàn hợp lý. Nhưng câu hỏi tôi không thể bỏ qua là: bản vá này đã chạy trên Odoo Online - nền tảng phục vụ hàng nghìn khách hàng trả tiền - từ nhiều tháng trước. Nếu nó đã đủ tin cậy để bảo vệ khách hàng trả tiền trên production, thì lý do gì nó chưa đủ tin cậy để phát hành cho cộng đồng open source? "In the near future" không có timeline cụ thể - có thể là tuần sau, có thể là năm sau. Và trong toàn bộ thời gian "battle-test" đó, mọi self-hosted Odoo instance đều đang phơi mình trước lỗ hổng mà vendor đã biết rõ.

So sánh với CVE-2021-44476 - cùng lỗ hổng, khác cách xử lý

Để thấy rõ sự bất nhất trong cách Odoo xử lý vấn đề bảo mật, hãy nhìn vào cách họ đã xử lý một lỗ hổng safe_eval tương tự trước đây:

CVE-2021-44476 Lỗ hổng này
Kỹ thuật Bypass qua request.httprequest.app Bypass qua str.format()
Tác động Đọc file server Đọc config, env vars, PG info
CVSS 6.8 (Medium) ~6.8 (Medium)
CVE phát hành? Không
Patch open source? (13.0, 14.0, 15.0) Không
Advisory công khai? (GitHub issue #107684) Không

Cùng mức độ nghiêm trọng, cùng loại lỗ hổng (sandbox escape trong safe_eval), cùng attack surface. Nhưng cách xử lý hoàn toàn khác nhau. Điều gì đã thay đổi giữa 2021 và 2026? Tôi không biết, nhưng rõ ràng không phải chính sách bảo mật trở nên tốt hơn.

Khuyến cáo cho người dùng Odoo self-hosted

Cho đến khi Odoo phát hành bản vá cho open source, đây là những gì bạn có thể làm ngay:

  1. Hạn chế quyền tạo Server Action - chỉ cấp cho admin đáng tin cậy nhất
  2. Giám sát bảng ir_actions_server - theo dõi các record mới được tạo, đặc biệt có chứa .format(
  3. Cân nhắc cấu hình unsafe_policy = raise trong file Odoo config (lưu ý: có thể ảnh hưởng một số tính năng)
  4. Không lưu secret quan trọng trong admin_passwd config - sử dụng biến môi trường và quản lý secret bên ngoài
  5. Giả định rằng bất kỳ admin nào cũng có thể đọc toàn bộ config server - thiết kế kiến trúc bảo mật dựa trên giả định này

Tự vá (workaround)

Nếu bạn muốn tự vá ngay, thêm kiểm tra format string vào assert_no_dunder_name() trong file odoo/tools/safe_eval.py:

Đây là workaround đơn giản nhắm vào attack vector cụ thể - bản vá chính thức của Odoo có thể phức tạp hơn để tránh false positives và cover thêm các edge cases khác.

Kết luận

Lỗ hổng kỹ thuật sẽ luôn tồn tại - đó là bản chất của phần mềm, và không ai trách Odoo vì có bug. Điều quan trọng là cách vendor phản ứng khi biết có lỗ hổng.

Odoo đã làm đúng khi vá nhanh cho SaaS. Nhưng quyết định giữ bản vá riêng cho khách hàng trả tiền, từ chối phát hành CVE, và không thông báo cho cộng đồng open source tạo ra một tình huống bất công rõ ràng: những người đóng góp code, báo bug, và xây dựng ecosystem cho Odoo lại là những người cuối cùng được bảo vệ.

Open source không chỉ là code miễn phí. Đó là cam kết minh bạch với cộng đồng. Khi bạn phát hành phần mềm dưới license open source, bạn chấp nhận trách nhiệm thông báo cho người dùng khi phần mềm đó có lỗ hổng - bất kể họ có trả tiền hay không. Bạn không được chọn ai xứng đáng được bảo vệ.

Lỗ hổng đã được report cho Odoo Security Team trước khi công bố. Họ xác nhận đã biết và chọn không phát hành CVE.

Denis Ledoux: If you are reading this, please reconsider your approach to security disclosures. The community deserves better.