Một kế toán viên chỉ có quyền đọc (read-only) có thể hủy hóa đơn và xóa lịch sử thanh toán trong công cụ kế toán mã nguồn mở Akaunting, khiến các doanh nghiệp nhỏ đối mặt với nguy cơ mất dữ liệu âm thầm. Lỗ hổng này đã được vá trong phiên bản 3.2.0, nhưng sai lầm—việc gắn các kiểm tra quyền hạn với một danh sách tên phương thức được mã hóa cứng (hard-coded)—vẫn đe dọa bất kỳ hệ thống nào dựa trên kiểm soát truy cập dựa trên vai trò (RBAC).

Lỗ hổng đã lọt qua như thế nào

API của Akaunting xác thực quyền của người dùng bằng cách tham chiếu đến một danh sách cho phép (allowlist) các tên phương thức. Danh sách này bao gồm các thao tác CRUD thông thường—tạo (create), đọc (read), cập nhật (update), xóa (delete)—nhưng lại bỏ sót một số điểm cuối (endpoints) thay đổi trạng thái:

  • markSent
  • markCancelled
  • markReceived

Vì các trình xử lý (handlers) đó không nằm trong danh sách, framework chưa bao giờ gọi quy trình kiểm tra quyền khi chúng được thực thi. Một người dùng với vai trò chỉ được đọc có thể gửi một yêu cầu GET đơn giản đến điểm cuối markCancelled và hệ thống sẽ coi đó là một thay đổi trạng thái hợp lệ.

Việc hủy một hóa đơn không chỉ đơn thuần là đánh dấu tài liệu là vô hiệu; nó còn xóa mọi hồ sơ thanh toán liên kết với hóa đơn đó. Kết quả là: một người dùng không có quyền chỉnh sửa có thể xóa sạch dấu vết tài chính của một giao dịch.

Kết quả kiểm thử cho thấy điều gì

Lỗ hổng xuất hiện trong Docker image chính thức của Akaunting:

  • Một yêu cầu PUT tiêu chuẩn để cập nhật hóa đơn trả về lỗi 403 Forbidden, xác nhận rằng lộ trình cập nhật thông thường đã được bảo vệ.
  • Một yêu cầu GET đến điểm cuối hủy hóa đơn đã thực hiện thành công mà không có lỗi ủy quyền, làm lộ ra lỗ hổng.

Tại sao điều này lại quan trọng

Các báo cáo tài chính có thể bị thay đổi mà không để lại dấu vết kiểm toán rõ ràng, khiến việc phát hiện gian lận trở nên khó khăn hơn và các sai sót vô ý cũng khó khắc phục hơn.

Cách khắc phục

Phiên bản 3.2.0 đã mở rộng bản đồ quyền hạn để bao gồm các hành động thay đổi trạng thái từng bị bỏ sót trước đó. Kể từ bản phát hành đó trở đi, bất kỳ yêu cầu nào thay đổi trạng thái của tài liệu—cho dù là đánh dấu đã gửi, đã hủy hay đã nhận—đều phải trải qua quá trình xác minh vai trò tương tự như một bản cập nhật tiêu chuẩn. Điều này khôi phục lại kỳ vọng rằng một vai trò chỉ được đọc thực sự không thể sửa đổi dữ liệu.

Bài học cho các nhà phát triển

  • Đừng bao giờ đánh đồng tên phương thức với sự an toàn. Việc thêm một điểm cuối mới không đồng nghĩa với việc nó tự động được bảo vệ; hãy kiểm tra từng phương thức công khai để tìm các tác dụng phụ (side effects).
  • Danh sách cho phép (allowlist) chỉ hoàn thiện bằng chính độ đầy đủ của danh sách đó. Một danh sách tĩnh các động từ "tốt" sẽ để ngỏ cửa cho những sai sót.
  • Tách biệt mục đích khỏi động từ HTTP. GET được dùng để chỉ đọc, nhưng ở đây nó lại thực hiện thay đổi trạng thái. Hãy giới hạn các thao tác thay đổi (mutations) vào POST, PUT, DELETE, PATCH.
  • Tự động hóa việc kiểm tra phạm vi bao phủ của quyền hạn. Các công cụ phân tích tĩnh có thể gắn cờ các phương thức controller thiếu lời gọi ủy quyền, giúp phát hiện lỗ hổng trước khi phát hành.
  • Kiểm thử với các tài khoản có đặc quyền tối thiểu. Bài kiểm tra dựa trên Docker đã sử dụng một người dùng chỉ được đọc; việc tái lập các kịch bản như vậy trong các đường ống CI sẽ giúp phát hiện sớm các vấn đề tương tự.

Cần lưu ý điều gì tiếp theo

Cộng đồng Akaunting đã phát hành phiên bản đã được vá lỗi. Các quản trị viên nên kiểm tra phiên bản thực thể (instance) của mình và tiến hành cập nhật ngay lập tức.

Đối với các nhà phát triển đang xây dựng bất kỳ hệ thống dựa trên vai trò nào, bài học rút ra rất rõ ràng: một mô hình phân quyền phụ thuộc vào việc phải ghi nhớ mọi hành động có thể xảy ra là một mô hình mong manh ngay từ thiết kế. Hãy khai báo rõ ràng các thao tác nào làm thay đổi trạng thái, thực thi kiểm tra ở cấp độ framework và thường xuyên kiểm tra mã nguồn. Chỉ khi đó, nhãn "chỉ được đọc" mới có thể được tin tưởng để giữ cho các hồ sơ tài chính được nguyên vẹn.