Ở phần form ta đã gặp lỗi kinh điển: quên csrf_token, POST trả về 400. Giờ mổ xẻ đúng cơ chế đó — CSRF (Cross-Site Request Forgery) — vì nó là tấm khiên mặc định cho mọi form POST trong Odoo, và hiểu sai nó dẫn tới hai thảm hoạ ngược nhau: hoặc form gãy không rõ lý do, hoặc tự tay tắt bảo vệ mở lỗ hổng. Bài này chỉ token hoạt động ra sao, khi nào được tắt, và vì sao jsonrpc không cần — kiểm chứng bằng ba lần POST thật trên cùng một route.

CSRF là gì và Odoo chặn ở đâu

Tấn công CSRF: một trang độc lừa trình duyệt của bạn — đang đăng nhập Odoo — gửi một POST tới Odoo (ví dụ "chuyển điểm cho tôi"). Trình duyệt tự đính kèm cookie phiên, nên server tưởng là bạn thật sự yêu cầu. Phòng thủ: mỗi form hợp lệ mang một token bí mật mà trang độc không đoán được; server từ chối POST không có token đúng.

Odoo kiểm ngay trong bộ điều phối request, trước khi gọi hàm route của bạn:

Ảnh chụp mã Python nền tối cơ chế CSRF thật của Odoo 19 trong file http py. Trong HttpDispatcher dispatch kiểm trước khi gọi hàm route: nếu method không thuộc SAFE_HTTP_METHODS và routing get csrf mặc định True thì lấy token bằng params pop csrf_token None, nếu không validate_csrf token thì raise BadRequest Session expired invalid CSRF token. Hàm csrf_token nhận time_limit, tính max_ts bằng int time cộng time_limit hoặc CSRF_TOKEN_SALT, msg bằng session sid 42 ký tự nối max_ts encode, hm bằng hmac new secret msg sha1 hexdigest, trả về hm chữ o max_ts. Chú thích validate_csrf so bằng consteq thời gian hằng chống dò, SAFE_HTTP_METHODS gồm GET HEAD OPTIONS TRACE không kiểm

Hình 1: Cơ chế thật. Với method không an toàn (POST/PUT/DELETE) và route có csrf=True (mặc định), Odoo lấy csrf_token khỏi tham số và validate_csrf; sai/thiếu là BadRequest 400. Token là HMAC-SHA1 của session.sid (42 byte đầu) + hạn dùng, ký bằng database.secret; so bằng consteq (thời-gian-hằng, chống dò từng ký tự). GET/HEAD/OPTIONS/TRACE không bao giờ bị kiểm — chúng không được đổi trạng thái.

Vì token gắn với session và ký bằng bí mật CSDL, một trang bên ngoài không thể tạo ra token hợp lệ cho phiên của bạn — đó là điều chặn được tấn công.

Cách khai và cạm bẫy

Route POST type='http' mặc định csrf=True. Việc của bạn chỉ là đặt token vào form:

Ảnh chụp mã nền tối. Phần đầu form action qcp_csrf co method post, input hidden name csrf_token t-att-value request csrf_token, chú thích form bắt buộc có csrf_token nếu không POST bị chặn. Route http qcp_csrf co type http auth public methods POST chú thích csrf mặc định True, hàm co_bao_ve trả về OK chú thích chỉ tới đây nếu token hợp lệ. Phần dưới route http qcp_csrf khong type http auth public methods POST csrf False chú thích tắt kiểm CSRF, hàm khong_bao_ve với chú thích chỉ dùng khi có cơ chế xác thực khác chữ ký HMAC của webhook Bearer token IP allowlist, trả về OK. Ghi chú type jsonrpc mặc định csrf tắt an toàn nhờ same-origin và body JSON, GET HEAD OPTIONS TRACE không bao giờ bị kiểm

Hình 2: Route trên (csrf mặc định) cần <input name="csrf_token" t-att-value="request.csrf_token()"/>. Route dưới đặt csrf=False — Odoo bỏ qua kiểm token hoàn toàn.

Ba lần POST thật cho thấy khác biệt rõ ràng:

Ảnh chụp terminal nền tối kết quả thật trên Odoo 19 cùng route khác token. Trường hợp a POST qcp_csrf co không token: lệnh curl X POST qcp_csrf co d x bằng 1 trả về HTTP 400 Bad Request và đoạn Session expired invalid CSRF token bị chặn. Trường hợp b POST qcp_csrf co có token hợp lệ scrape từ form: POST với csrf_token và x bằng 1 trả về HTTP 200 OK route CÓ bảo vệ CSRF token hợp lệ. Trường hợp c POST qcp_csrf khong csrf False không token: curl X POST qcp_csrf khong d x bằng 1 trả về HTTP 200 OK route KHONG bao ve CSRF bo qua token. Ghi chú csrf True token sai thiếu bằng 400, csrf False chạy vô điều kiện

Hình 3: Cùng route /qcp_csrf/co: thiếu token → 400 "Session expired (invalid CSRF token)"; token hợp lệ → 200. Route /qcp_csrf/khong (csrf=False) chạy vô điều kiện dù không token.

Cạm bẫy hay gặp:

  • Quên token → form gãy 400 mà thông báo "Session expired" gây hiểu nhầm là do đăng nhập. Luôn thêm dòng csrf_token vào mọi form POST.
  • Token gắn session. Nếu render form trong một phiên rồi POST ở phiên khác (ví dụ phiên ẩn danh không được lưu bền), token không khớp → 400. Đây là lý do form công khai cho khách chưa đăng nhập đôi khi trục trặc nếu session không ổn định.

Khi nào csrf=False là đúng

csrf=False không phải cách "sửa lỗi 400" — tắt nó là bỏ tấm khiên. Chỉ đặt khi request không đến từ trình duyệt người dùng và đã có cơ chế xác thực khác:

  • Webhook máy-gọi-máy: dịch vụ ngoài POST vào, xác thực bằng chữ ký HMAC trong header hoặc Bearer token, không có cookie phiên để mà giả mạo.
  • API dùng token/khoá: đã kiểm Authorization/X-API-Key thì CSRF không thêm gì.

Nguyên tắc: form web cho người dùng → luôn csrf=True; endpoint máy gọi có xác thực riêng → csrf=False và phải tự bảo vệ bằng cách khác.

Vì sao jsonrpc không cần CSRF

Route type='jsonrpc' (front-end OWL gọi server) mặc định tắt CSRF — và đó là đúng, không phải lỗ hổng. Lý do: request JSON-RPC gửi với Content-Type: application/json, mà một form HTML độc không thể đặt Content-Type đó qua request "đơn giản" (simple request) — trình duyệt sẽ bắt buộc preflight CORS, và same-origin policy chặn trang khác đọc/gửi. Nói cách khác, bản thân việc yêu cầu body JSON đã là một rào chắn same-origin. Vì vậy rpc() của OWL không phải đính token.

Ba ý mang về

  1. POST type='http' mặc định csrf=True — Odoo kiểm csrf_token trong HttpDispatcher trước khi vào hàm route; thiếu/sai là 400 "Session expired (invalid CSRF token)". Luôn đặt <input name="csrf_token" t-att-value="request.csrf_token()"/> trong form.
  2. Token là HMAC gắn session (ký bằng database.secret, so bằng consteq), nên trang ngoài không tạo được token hợp lệ; GET/HEAD/OPTIONS/TRACE không bị kiểm.
  3. csrf=False chỉ cho endpoint máy-gọi-máy có xác thực khác (chữ ký/Bearer), không phải mẹo chữa 400; jsonrpc tự an toàn nhờ same-origin + body JSON nên không cần token.

Ta đã nhắc tới "session" nhiều lần mà chưa mổ kỹ. Phần sau đào sâu request.env và session trong controller — env được dựng ra sao theo auth, session lưu gì và ở đâu, và cách đọc/ghi session an toàn để giữ trạng thái giữa các request.