Ở report QWeb phía server, t-out in ra HTML thô — bạn dùng nó để nhúng nội dung có định dạng. Nên khi sang OWL (giao diện trình duyệt), nhiều người quen tay dùng t-out và bất ngờ: nó không render HTML nữa, mà hiện ra nguyên chuỗi <b>...</b> như văn bản. Không phải bug — đó là một hàng rào chống XSS có chủ đích. Bài này dựng demo thật để thấy rõ ba trường hợp, và khi nào HTML mới thật sự được render.

Ba cách đổ dữ liệu vào HTML

Tôi thêm vào dashboard hai giá trị: một chuỗi thường chứa HTML, và một giá trị markup() (đánh dấu là HTML tin cậy), rồi in chúng bằng t-esc và t-out:

Ảnh chụp đoạn mã nền tối hai phần. Phần trên component import markup từ @odoo/owl, this state useState với demo_raw là chuỗi thẻ b Vàng ngôi sao chú thích chuỗi HTML thô, và demo_markup là markup của cùng chuỗi chú thích đánh dấu là HTML tin cậy. Phần dưới template QWeb-OWL, span t-esc state demo_raw chú thích t-esc luôn escape hiện chữ thẻ b Vàng, span t-out state demo_raw chú thích t-out chuỗi vẫn escape OWL an toàn mặc định, span t-out state demo_markup chú thích t-out markup render HTML chữ Vàng in đậm, rồi div t-if state vang lớn hơn 0 in có t-esc state vang thẻ Vàng và div t-else chưa có thẻ Vàng

Hình 1: Ba cách in. t-esc="state.demo_raw" — luôn escape, biến < > thành thực thể HTML. t-out="state.demo_raw" (chuỗi thường) — cũng escape trong OWL. Chỉ t-out="state.demo_markup" (giá trị đã bọc markup()) mới render thành HTML thật. Phần dưới còn có t-if/t-else chọn nhánh theo state.vang.

Kết quả: chỉ markup mới thành HTML

Render dashboard trong trình duyệt, ba dòng cho ba kết quả khác nhau:

Ảnh chụp dashboard OWL thật, phía trên là ba thẻ số 4 tổng thẻ 385 điểm 1 Vàng, phía dưới là khối demo t-esc vs t-out nền kem, dòng 1 t-esc chuỗi hiện nguyên văn thẻ b Vàng thẻ đóng b ngôi sao, dòng 2 t-out chuỗi cũng hiện nguyên văn thẻ b Vàng thẻ đóng b ngôi sao, dòng 3 t-out markup hiện chữ Vàng IN ĐẬM kèm ngôi sao, và dòng t-if màu xanh có 1 thẻ Vàng dấu tích

Hình 2: Ba dòng, ba kết quả trên giao diện thật. Dòng 1 (t-esc chuỗi) và dòng 2 (t-out chuỗi) đều hiện nguyên văn <b>Vàng</b> ⭐ — HTML bị escape thành chữ. Chỉ dòng 3 (t-out markup) hiện chữ Vàng in đậm — HTML thật sự được render. Dòng cuối là t-if chạy đúng nhánh (có 1 thẻ Vàng). Chụp bằng trình duyệt thật, không có lỗi Console.

Đối chiếu và lý do bảo mật

Gom lại thành bảng cho dễ nhớ:

Ảnh bảng đối chiếu trên nền kem tiêu đề t-esc và t-out trong OWL khi nào escape khi nào render HTML, ba dòng: t-esc chuỗi cho kết quả hiện chữ thẻ b Vàng an toàn màu xanh, t-out chuỗi cho kết quả hiện chữ thẻ b Vàng an toàn màu xanh, t-out markup cho kết quả Vàng HTML render với cột an toàn ghi chỉ tin nội dung màu đỏ; dưới bảng ghi chú điểm dễ sai trong report QWeb server t-out render HTML thô nhưng trong OWL trình duyệt t-out chuỗi thường vẫn escape muốn render HTML phải bọc markup, đây là hàng rào chống XSS dữ liệu người dùng nhập không bao giờ thành HTML trừ khi chủ động markup

Hình 3: t-esc và t-out với chuỗi thường đều escape — an toàn. Chỉ t-out với markup() mới render HTML, và đó là chỗ bạn phải cẩn thận. Lý do thiết kế: chống XSS. Nếu state.demo_raw là tên khách hàng do người dùng nhập, mà nó chứa <script>..., thì escape mặc định khiến nó chỉ hiện thành chữ vô hại. Muốn cho phép HTML, bạn phải chủ động gọi markup() — và khi đó trách nhiệm làm sạch là của bạn.

Vài lưu ý khi làm template OWL

  • Mặc định luôn dùng t-esc (hoặc t-out chuỗi). Chỉ bọc markup() khi nội dung chắc chắn an toàn (HTML do chính bạn sinh, không phải dữ liệu người dùng). Đừng markup() một chuỗi có phần người dùng nhập chưa làm sạch.
  • t-esc vs t-out: khác biệt chỉ lộ với markup. Với chuỗi/số thường, hai cái cho kết quả y hệt. t-out tồn tại để xử lý được cả markup; t-esc thì tuyệt đối không.
  • t-if/t-elif/t-else là thật của DOM. Nhánh sai không được dựng ra (khác d:none); dùng để bật/tắt cả khối theo state.
  • Đây khác report QWeb server. Ở report (_render_qweb_pdf), t-out render HTML thô. Đừng mang thói quen đó sang OWL — cùng tên chỉ thị nhưng hành vi bảo mật khác.

Ba ý mang về

  1. t-esc luôn escape; t-out với chuỗi thường CŨNG escape trong OWL — chỉ t-out với giá trị markup() mới render HTML thật.
  2. Đây là hàng rào chống XSS: dữ liệu (nhất là do người dùng nhập) không bao giờ thành HTML trừ khi bạn chủ động bọc markup() và tự chịu trách nhiệm làm sạch.
  3. t-if/t-elif/t-else bật/tắt cả khối DOM theo state; và nhớ hành vi t-out ở OWL khác report QWeb server.

Đổ một giá trị vào HTML thì xong. Nhưng dữ liệu thật thường là danh sách — 4 thẻ, mỗi thẻ một dòng. Phần sau đi vào t-foreach hiển thị danh sách trong OWL: lặp mảng, và vì sao t-key là bắt buộc.