Các lỗi row-level security của Supabase trong ứng dụng do AI xây dựng
Những lỗi row-level security (RLS) của Supabase xuất hiện nhiều nhất trong ứng dụng do AI xây dựng là bảng tắt RLS, chính sách cho phép tất cả mọi người, chính sách cho phép bất kỳ người dùng đã đăng nhập nào, và phân quyền dựa trên dữ liệu người dùng có thể sửa. Phần lớn có chung một nguyên nhân. Khi một truy vấn thất bại với lỗi về quyền, thay đổi nhỏ nhất khiến ứng dụng chạy được là một chính sách lỏng hơn, và một AI agent được yêu cầu sửa lỗi “dữ liệu của tôi không tải được” thường sẽ thực hiện đúng thay đổi đó.
Những lỗi này quan trọng hơn trong ứng dụng từ Lovable, Bolt và các công cụ tương tự, vì các ứng dụng đó thường không có mã máy chủ nằm giữa trình duyệt và cơ sở dữ liệu. Trình duyệt truy vấn Supabase bằng một khóa công khai, và các chính sách RLS quyết định mỗi yêu cầu có thể truy cập những gì. Một chính sách bị thiếu sẽ làm lộ toàn bộ bảng, chứ không phải một endpoint.
1. RLS bị tắt
Các bảng được tạo trong dashboard của Supabase được bật RLS theo mặc định. Các bảng được tạo bằng SQL migration, tức là cách các công cụ xây dựng bằng AI tạo bảng, chỉ có RLS nếu migration bật nó. Một bảng không có RLS có thể bị đọc và thay đổi bởi bất kỳ ai nắm giữ khóa công khai. Đây là nguyên nhân của CVE-2025-48757, trong đó một nhà nghiên cứu tìm thấy dữ liệu bị lộ ở 170 trong số 1,645 ứng dụng Lovable mà ông kiểm tra.
Để kiểm tra, hãy liệt kê các bảng public có rowsecurity là false trong pg_tables, hoặc mở Security Advisor của Supabase.
2. Chính sách cho phép tất cả mọi người
using (true) trên một chính sách select khiến bảng trở nên công khai. Điều đó đúng với dữ liệu mà bạn vốn sẽ công bố, như bài blog hoặc danh mục sản phẩm. Với mọi thứ khác, đó là rò rỉ, và nó thường xuất hiện như một cách sửa cho trang trống. Hãy tìm using (true) và with check (true) trong các migration của bạn và xác nhận từng chỗ đều là chủ ý.
3. Coi “đã đăng nhập” là “được phép”
Một chính sách như using (auth.role() = 'authenticated') giới hạn bảng cho người dùng đã đăng nhập. Trong một ứng dụng mở đăng ký tự do, bất kỳ ai cũng có thể trở thành người dùng đã đăng nhập, tạo tài khoản và đọc mọi hàng. Một chính sách bảo vệ dữ liệu theo từng người dùng sẽ so sánh chủ sở hữu của hàng với người gọi, ví dụ (select auth.uid()) = user_id.
4. Thao tác chèn không kiểm tra chủ sở hữu
Một chính sách insert có with check (true) cho phép người dùng tạo các hàng thuộc về người khác, chẳng hạn một tin nhắn gán cho người dùng khác hoặc một đơn hàng trên tài khoản của khách hàng khác. Mỗi chính sách insert cần một with check gắn hàng mới với người gọi.
5. Trông đợi RLS bảo vệ cột
RLS kiểm soát những hàng mà người dùng có thể tác động. Nó không kiểm soát cột. Nếu người dùng có thể cập nhật hàng profiles của chính họ và hàng đó có is_admin, plan hoặc credits, họ có thể thay đổi các trường này từ trình duyệt. Hãy chuyển các trường đặc quyền sang một bảng mà người dùng không thể ghi, hoặc giới hạn những cột mà vai trò authenticated được phép cập nhật:
revoke update on public.profiles from authenticated;
grant update (display_name, avatar_url) on public.profiles to authenticated;
6. Phân quyền dựa trên siêu dữ liệu người dùng
Người dùng có thể tự sửa user_metadata của mình (được lưu dưới dạng raw_user_meta_data). Một chính sách hoặc hàm đọc vai trò từ đó sẽ cho phép người dùng tự biến mình thành quản trị viên. Supabase khuyến nghị dùng app_metadata cho dữ liệu phân quyền, vì chỉ máy chủ của bạn mới thay đổi được nó. Một bảng vai trò được RLS bảo vệ cũng dùng được.
7. View và hàm bỏ qua RLS
- View chạy với quyền của người tạo, thường là
postgres, nên theo mặc định chúng bỏ qua RLS trên các bảng bên dưới. Trên Postgres 15 trở lên, hãy tạo chúngwith (security_invoker = true). - Hàm security definer chạy với đặc quyền của người tạo. Nếu một hàm như vậy nằm trong schema bị lộ, bất kỳ ai cũng có thể gọi nó tại
/rest/v1/rpc/<name>. Trong nghiên cứu về CVE-2025-48757, hàm RPC của một ứng dụng đã trả về một Gemini API key cho bất kỳ ai gọi nó. Hãy giữ các hàm như vậy ngoài các schema bị lộ, hoặc kiểm tra người gọi bên trong hàm.
8. Khóa bí mật nằm trong trình duyệt
Khóa bí mật của Supabase (sb_secret_...) và khóa service_role cũ bỏ qua hoàn toàn RLS, nên nếu một trong hai lọt vào mã front-end, không chính sách nào ở trên còn có tác dụng. Chỉ publishable key (hoặc khóa anon cũ) mới nên nằm trong trình duyệt. Khóa nào là khóa nào, và phải làm gì nếu một khóa bị rò rỉ, được trình bày ở bước 4 trong danh sách kiểm tra trước khi ra mắt cho ứng dụng Lovable của Cyfendo.
Kiểm thử các chính sách của bạn
- Tạo hai người dùng thử, A và B, mỗi người có dữ liệu riêng.
- Khi chưa đăng nhập, gửi yêu cầu lấy từng bảng chỉ với khóa công khai. Không nên có dữ liệu riêng tư nào được trả về.
- Đăng nhập là B, thử đọc, cập nhật và xóa các hàng của A, và thử chèn một hàng thuộc sở hữu của A.
- Với tư cách người dùng thông thường, thử cập nhật
is_admin,planhoặc các trường tương tự của chính bạn. - Khi chưa đăng nhập, gọi từng hàm trong
/rest/v1/rpc/.
Lặp lại sau mọi thay đổi đối với schema hoặc chính sách. Trong một ứng dụng mà AI agent vẫn đang xây dựng, việc đó có thể diễn ra vài lần mỗi ngày.
Cyfendo kiểm thử RLS như thế nào
Khi các tệp migration Supabase của một dự án nằm trong phần mã được kiểm tra, Cyfendo khởi chạy một Supabase thay thế trong một sandbox cách ly, áp dụng các cài đặt và chính sách RLS của dự án, rồi tạo hai người dùng thử. Sau đó Cyfendo thực hiện các yêu cầu ở bước 2 và 3: đọc và thay đổi các hàng chỉ với khóa công khai, và đọc hoặc thay đổi các hàng của một người dùng với tư cách người dùng kia. Những vấn đề tái hiện được sẽ được báo cáo kèm các bước sửa. Cyfendo hiểu các chính sách kiểm tra chủ sở hữu, true và auth.role(); mọi chính sách không phân tích được đều được coi là từ chối truy cập, nên Cyfendo không báo cáo những điểm rò rỉ chưa tái hiện được. Khi được kết nối với Bolt hoặc Lovable, Cyfendo chạy khi agent thay đổi quyền truy cập dữ liệu.