# Cách bảo mật ứng dụng Lovable trước khi ra mắt

Cyfendo · 20 tháng 10, 2026

Bảo mật một ứng dụng Lovable trước khi ra mắt chủ yếu là bảo mật cơ sở dữ liệu Supabase của nó. Hãy bật row-level security (RLS) cho mọi bảng, viết một chính sách cho mỗi thao tác để giới hạn người dùng trong các hàng của chính họ, và kiểm thử kết quả bằng cách đăng nhập với hai người dùng khác nhau. Sau đó, giữ khóa bí mật ngoài trình duyệt, buộc edge function xác định người gọi từ phiên đăng nhập, chỉ lấy trạng thái thanh toán từ webhook của nhà cung cấp dịch vụ thanh toán, và chạy một bản quét trước khi xuất bản.

## 1. Tìm mọi bảng không có RLS

Mã chạy trên trình duyệt của ứng dụng Lovable giao tiếp trực tiếp với Supabase bằng một khóa công khai, nên RLS là thứ đứng giữa khóa đó và dữ liệu của bạn. Trong trình soạn thảo SQL của Supabase, hãy liệt kê các bảng trong schema `public` chưa có RLS:

```sql
select tablename
from pg_tables
where schemaname = 'public' and rowsecurity = false;
```

Bất kỳ ai mở ứng dụng của bạn đều có thể đọc và thay đổi mọi bảng mà truy vấn này trả về. Hãy bật RLS cho từng bảng:

```sql
alter table public.notes enable row level security;
```

Security Advisor của Supabase và bản quét khi xuất bản của Lovable báo cáo cùng một điều, nên bạn có thể dùng một trong hai để xác nhận danh sách đã trống.

## 2. Viết một chính sách cho mỗi thao tác

Một bảng đã bật RLS nhưng không có chính sách nào sẽ không trả về gì cho bất kỳ ai. Khi điều đó làm hỏng một trang, thay đổi nhanh nhất để trang chạy được là một chính sách cho phép tất cả mọi người, và đó là một cách phổ biến khiến RLS mất tác dụng. Thay vào đó, hãy viết một chính sách cho mỗi thao tác, mỗi chính sách gắn với người dùng đã đăng nhập:

```sql
create policy "Read own notes" on public.notes
  for select to authenticated using ((select auth.uid()) = user_id);

create policy "Add own notes" on public.notes
  for insert to authenticated with check ((select auth.uid()) = user_id);

create policy "Change own notes" on public.notes
  for update to authenticated
  using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);

create policy "Delete own notes" on public.notes
  for delete to authenticated using ((select auth.uid()) = user_id);
```

`using` kiểm soát những hàng hiện có mà người dùng có thể xem hoặc sửa. `with check` kiểm soát những hàng họ có thể tạo ra hoặc nhận được sau thao tác, nhờ vậy người dùng không thể chèn một ghi chú dưới ID của người khác. Supabase khuyến nghị viết `(select auth.uid())` thay vì `auth.uid()` để giá trị được tính một lần cho mỗi truy vấn thay vì một lần cho mỗi hàng.

Có hai dạng chính sách đáng xem lại mỗi khi bạn gặp:

- `using (true)` khiến ai cũng đọc được bảng. Nó phù hợp với dữ liệu thực sự công khai, như các bài đã đăng, và chỉ dùng cho `select`.
- `using (auth.role() = 'authenticated')` cho phép mọi người dùng đã đăng nhập đọc mọi hàng. Nếu đăng ký mở tự do, điều đó bao gồm bất kỳ ai.

Có thêm các mẫu tương tự trong [Các lỗi row-level security của Supabase trong ứng dụng do AI xây dựng](/blog/supabase-row-level-security-mistakes).

## 3. Kiểm thử khi chưa đăng nhập, với tư cách người dùng A và người dùng B

Tạo hai tài khoản thử, mỗi tài khoản có một ít dữ liệu, rồi kiểm tra:

1. **Chưa đăng nhập**, trong cửa sổ riêng tư: không có dữ liệu riêng tư nào được tải.
2. **Với tư cách người dùng A:** chỉ dữ liệu của A xuất hiện.
3. **Với tư cách người dùng B:** không thể đọc hoặc thay đổi dữ liệu của A, kể cả bằng cách sửa ID trong URL hoặc trong một yêu cầu.

Bạn cũng có thể truy vấn API chỉ với khóa công khai. Với những hàng mà một yêu cầu không được phép xem, RLS trả về danh sách rỗng chứ không báo lỗi:

```bash
curl "https://YOUR-PROJECT.supabase.co/rest/v1/notes?select=*" \
  -H "apikey: YOUR_PUBLISHABLE_KEY"
```

## 4. Giữ khóa bí mật ngoài trình duyệt

Supabase cấp hai loại khóa ([tài liệu Supabase](https://supabase.com/docs/guides/api/api-keys)):

| Khóa | Trông như thế nào | An toàn trong mã chạy trên trình duyệt? | Tuân theo RLS? |
|---|---|---|---|
| Publishable key | `sb_publishable_...` | Có | Có |
| Legacy anon key | token dài bắt đầu bằng `eyJ` | Có | Có |
| Secret key | `sb_secret_...` | Không | Không, nó bỏ qua RLS |
| Legacy service_role key | token dài bắt đầu bằng `eyJ` | Không | Không, nó bỏ qua RLS |

Publishable key vốn được thiết kế để công khai; chính RLS khiến việc đó an toàn. Một khóa bí mật trong mã front-end cho mọi khách truy cập toàn quyền vào cơ sở dữ liệu của bạn. Hai khóa cũ trông giống nhau, và điểm khác biệt là claim `role` bên trong token (`anon` hoặc `service_role`). Supabase dự định ngừng dùng các khóa cũ trước cuối năm 2026.

Các công cụ build cũng dựa vào tên biến để quyết định thứ gì đến được trình duyệt. Mọi thứ có tiền tố `VITE_` (thiết lập của Lovable), `NEXT_PUBLIC_` hoặc `PUBLIC_` đều được đóng gói vào mã front-end. Điều đó ổn với URL Supabase và publishable key của bạn, nhưng là rò rỉ đối với khóa bí mật của Stripe (`sk_live_...`), API key của nhà cung cấp AI hoặc secret dùng để ký webhook. Hãy tìm các giá trị này trong mã của bạn, rồi mở JavaScript của trang đã xuất bản trong công cụ dành cho nhà phát triển của trình duyệt và tìm ở đó nữa.

Nếu một khóa bí mật từng bị lộ, hãy thay nó ngay cả sau khi đã xóa khỏi mã, vì các bản build cũ và lịch sử Git của bạn vẫn chứa nó. Tạo khóa mới, chuyển mọi máy chủ và function sang khóa đó, xác nhận không còn gì dùng khóa cũ, rồi xóa khóa cũ.

## 5. Buộc edge function xác định người gọi

Một edge function nhận `user_id` trong thân yêu cầu sẽ hành động thay cho bất kỳ ID nào nó nhận được. Hãy tạo Supabase client bằng header `Authorization` của yêu cầu, gọi `supabase.auth.getUser()`, từ chối yêu cầu nếu không có người dùng nào được trả về, và dùng ID của người dùng đó.

## 6. Chỉ lấy trạng thái thanh toán từ nhà cung cấp dịch vụ thanh toán

Chỉ đánh dấu một đơn hàng đã thanh toán, hoặc mở khóa một gói, trong trình xử lý webhook có xác minh chữ ký của nhà cung cấp (với Stripe là header `Stripe-Signature`). Thông báo thành công từ trình duyệt có thể bị làm giả.

## 7. Không lưu vai trò trong siêu dữ liệu người dùng

Người dùng Supabase có thể tự sửa `user_metadata` của mình. Một chính sách hoặc bước kiểm tra đọc vai trò từ đó sẽ cho phép người dùng tự nâng quyền. Hãy lưu vai trò trong `app_metadata`, nơi chỉ máy chủ của bạn mới thay đổi được, hoặc trong một bảng được RLS bảo vệ.

## 8. Giữ tệp riêng tư trong bucket riêng tư

Bất kỳ ai có liên kết đều có thể đọc một tệp trong storage bucket công khai. Hãy đặt tệp riêng tư vào bucket riêng tư, với các chính sách trên `storage.objects` giới hạn mỗi người dùng trong thư mục của chính họ.

## 9. Quét trước khi xuất bản

Bản quét nhanh của Lovable chạy khi bạn mở hộp thoại xuất bản và bao gồm các quy tắc cơ sở dữ liệu, lỗ hổng nghiêm trọng trong thư viện phụ thuộc và việc lộ MCP. Bản quét sâu xem xét toàn bộ cơ sở mã nhưng chỉ chạy khi bạn bắt đầu nó từ chế độ xem Security, vì vậy hãy chạy nó trước khi ra mắt. Lặp lại các bước 3 và 9 sau mọi thay đổi đáng kể liên quan đến đăng nhập, truy cập dữ liệu hoặc thanh toán.

## Chạy các bước kiểm tra này cho mọi thay đổi

Cyfendo kết nối với Lovable dưới dạng MCP connector và kiểm tra mỗi tác vụ chạm đến đăng nhập, truy cập dữ liệu, chính sách Supabase, thanh toán, tải lên hoặc thông tin bí mật. Với ứng dụng Supabase, Cyfendo dựng một cơ sở dữ liệu thay thế từ các tệp migration của bạn, với hai người dùng thử, rồi lặp lại các yêu cầu ở bước 3. Những vấn đề Cyfendo tái hiện được sẽ trả về khung chat Lovable kèm các bước sửa, và Lovable có thể yêu cầu Cyfendo một bản vá được kiểm thử với chính các yêu cầu đó. Xem [hướng dẫn thiết lập Lovable](/docs/integration/lovable).

Source: https://cyfendo.com/vi/blog/how-to-secure-a-lovable-app
