SteelreedSteelreed

Blog · · Nguyễn Quang Vinh

15 lỗi lộ dữ liệu tenant trong 75 câu query

Bọn mình chạy QueryFence trên một ứng dụng Spring Boot không được viết để chiều nó. Nó kiểm 75 câu lệnh, tìm ra 15 lỗi lộ dữ liệu tenant thật, và báo nhầm 4 lần.

Với SaaS phục vụ nhiều khách hàng trên cùng một database, câu query nào đụng tới bảng của khách cũng phải có WHERE tenant_id = ?. Quên một lần là khách A thấy đơn hàng của khách B. Review code hiếm khi bắt được, vì thiếu một điều kiện là một thứ vắng mặt, rất khó nhìn ra. Test thì vẫn xanh, vì dữ liệu test thường chỉ có một tenant.

QueryFence kiểm tra các câu SQL mà integration test thật sự gửi xuống database, và cho build fail ở đúng câu quên lọc tenant. Trước khi phát hành 0.1.0, bọn mình muốn biết nó xử lý thế nào với code không được viết để chiều nó, nên đã chạy thử trên Northwind Shop: một ứng dụng mẫu riêng trong repo QueryFence, viết theo kiểu một codebase Spring Boot thật đã lớn lên vài năm, có Spring Data JPA, JPQL, native query và mapper MyBatis.

Cách chạy thử

Bọn mình làm đúng theo hướng dẫn áp dụng, như một người dùng mới:

  1. Thêm dependency queryfence-spring-test.
  2. Lấy danh sách bảng có tenant từ information_schema, viết policy với mode: REPORT để chưa làm fail gì cả.
  3. Chạy các test mà ứng dụng đã có sẵn. Không thêm hay sửa test nào.
  4. Đọc báo cáo và xét từng kết quả xem có phải lỗi thật không.

Con số

queryfence.yml [REPORT]: 20 findings, 75 statements, 62 tests

Kết luận
  lỗi lộ dữ liệu thật     15
  báo nhầm                 4
  ngoại lệ có chủ đích     1

Mỗi kết quả đến từ một chỗ khác nhau trong code, nên mỗi cái là một lần sửa riêng. Quan trọng không kém: mọi câu query viết đúng đều qua. Derived query có tenant, phân trang kèm câu count(*), join được lọc qua bảng bên kia, subquery exists, CTE có điều kiện bên trong, và SQL động của MyBatis giữ được tenant đều không bị báo.

Những lỗi đáng xem kỹ

Phần lớn trong 15 lỗi là những thủ phạm quen mặt: một findByStatus còn sót từ bản prototype đầu tiên, một findAll() trên bảng có tenant, một lệnh update hàng loạt chạy qua mọi tenant, một findByNumber dựa vào chuyện số hoá đơn là duy nhất, mà duy nhất không có nghĩa là đã tách tenant, và một findById từ màn hình hỗ trợ khách hàng. Có hai lỗi thuộc loại người review gần như không bao giờ thấy:

Điều kiện MyBatis biến mất lúc chạy

Mapper dựng mệnh đề WHERE bằng <if>. Khi tham số tenant là null, điều kiện lặng lẽ biến mất và câu query trả về đơn hàng của mọi tenant. Cùng một câu trong mapper, lần gọi này an toàn, lần gọi sau thì không, nên đọc file XML sẽ không thấy. Chỉ kiểm câu thật sự đã chạy mới bắt được.

Nửa sau của UNION ALL

Một báo cáo đối soát có hai nhánh. Nhánh đầu có p.tenant_id = ?, nên nhìn qua tưởng đã lọc. Nhánh thứ hai cộng hoá đơn chưa thanh toán của tất cả tenant. QueryFence kiểm từng nhánh riêng, nên đã bắt được nhánh thứ hai.

Những chỗ QueryFence sai

Có 4 lần báo nhầm, tức 20% số kết quả, hay khoảng 5% số câu lệnh:

QueryFence cố ý chọn cách "không chắc thì báo": khi không chứng minh được một câu là an toàn, nó nói ra thay vì im lặng. Bọn mình thà để bạn thấy 4 lần báo nhầm còn hơn bỏ lọt một lỗi lộ dữ liệu.

Những gì đã sửa trước bản 0.1.0

Lần chạy thử này cũng tìm ra lỗi của chính QueryFence, và những lỗi chính đã được sửa trước khi phát hành: tuỳ chọn onUnparseable giờ đã có tác dụng, bản tóm tắt trên console đã hiện cho người dùng Maven, và câu không phân tích được giờ nêu tên các bảng cần bảo vệ mà nó nhắc tới, thay vì giấu đi.

Dùng thử

QueryFence 0.1.0 đã có trên Maven Central. Với dự án Spring Boot, chỉ cần thêm một dependency test và một file policy. Không phải thêm annotation, không phải sửa code test.

<dependency>
  <groupId>com.steelreed</groupId>
  <artifactId>queryfence-spring-test</artifactId>
  <version>0.1.0</version>
  <scope>test</scope>
</dependency>

Với dự án đang chạy, hãy bắt đầu ở chế độ REPORT, đọc những gì nó tìm thấy, rồi mới chuyển sang FAIL. Hướng dẫn bắt đầu mất khoảng năm phút, báo cáo đầy đủ liệt kê từng kết quả, còn mã nguồn ở trên GitHub. Nếu bạn tìm được kiểu câu query nào lọt qua được, hãy báo bọn mình: đó là lỗi bọn mình muốn nghe nhất.

Số liệu lấy từ báo cáo chạy thử ngày 24/09/2026. Bài viết có AI hỗ trợ và đã đối chiếu với báo cáo. Read in English.