Vibe coding 上線前資安檢查:金鑰、資料庫權限與付款的 8 個項目

Indie Hackers Taiwan

登入、資料儲存和付款都能正常操作,還不代表權限設定正確。最值得先試的是:未登入的人能看到什麼?帳號 B 能不能讀到帳號 A 的資料?前端改一個參數,能不能把自己變成管理員?

下面整理 8 項上線前的檢查。請在自己的測試環境使用測試資料;修改或刪除的測試不要直接拿真實用戶資料操作。這份清單適合先找出常見漏洞,不能當成完整的資安認證。

先準備兩個測試帳號

建立帳號 A、帳號 B,另外開一個未登入的瀏覽器視窗。用 A 建立一筆私人資料,再分別用 B 和未登入狀態嘗試存取。記下預期結果與實際結果,後面的資料庫、API 與檔案檢查都沿用這三種身分。

1. 前端有沒有秘密金鑰?

凡是會被打包進瀏覽器的值,使用者都看得到。Next.js 中供瀏覽器程式使用的 NEXT_PUBLIC_ 變數會在建置時寫進 JavaScript;Vite 預設也會把 VITE_ 變數提供給瀏覽器程式。不要用這些前綴存秘密。Next.js 文件、Vite 文件

先建置一次,再搜尋專案實際產生的目錄。下例會忽略不存在的目錄,只印出檔名:

npm run build
for dir in .next/static dist build; do
  if [ -d "$dir" ]; then
    grep -RIlE 'sk_live_|sk-proj-|sk-ant-|sb_secret_|service_role|AKIA[0-9A-Z]{16}|AIza' "$dir"
  fi
done
金鑰 能不能放前端
Stripe secret key(sk_live_) 不能
OpenAI、Anthropic、Gemini API key 不能
Supabase secret key、舊版 service_role 不能
Supabase publishable key、舊版 anon key 可以,但暴露給 Data API 的資料表須設定 RLS 與適當的操作權限
Firebase Web API key(常見前綴 AIza) 限制為 Firebase 相關 API 時可以公開;資料權限靠 Security Rules

搜尋到 AIza 時要分辨來源。Firebase Web 設定裡的 key 屬於正常情況,Firebase 文件說明這類 key 不是用來控管後端存取;Gemini API key 可能使用相同前綴,不能放進瀏覽器或公開程式碼。私人金鑰應放在伺服器端的秘密設定中。Firebase 公開金鑰的 API 允許清單不要加入 Gemini;需要 Gemini 時另外建立並保護專用金鑰。

這個搜尋只能找到部分常見格式。沒有命中不代表沒有外洩,還要檢查建置設定、source map 與 Git 歷史。Firebase 的 API key 說明也區分了識別專案與授權存取的用途。

把需要秘密金鑰的呼叫移到伺服器端,例如 API route、server action 或 Edge Function,再讓前端呼叫自己的 API。這個 API 仍要檢查登入與用量,否則只是把可濫用的入口換了位置。已經公開的秘密金鑰要撤銷並換新。

2. 資料庫有沒有擋住未授權的讀寫?

Supabase

RLS 是 Row Level Security,用規則限制「這個使用者可以存取哪些資料列」。Supabase 要求暴露給 Data API 的 schema(預設為 public)內的資料表啟用 RLS。

資料表權限先決定角色能否執行查詢,RLS 再決定能讀哪些資料列。缺少權限可能回傳錯誤;具有讀取權限、但沒有任何 RLS 規則允許讀取時,則可能回傳空陣列。不要只看到空陣列就認定設定正確。來源:Supabase 金鑰與權限說明

啟用 RLS 不會自動撤回角色原本擁有的資料表權限。也要檢查 anon 與 authenticated,只保留它們實際需要的操作權限。來源:Supabase RLS 文件

在 SQL Editor 列出狀態:

select schemaname, tablename, rowsecurity
from pg_tables
where schemaname = 'public';

對暴露給 Data API 的資料表,先處理 rowsecurity 為 false 的項目。若你另有公開的 schema,也要一起檢查。接著用測試專案的 publishable key 實際查詢,確認未登入時拿不到私人資料:

curl "https://YOUR_PROJECT.supabase.co/rest/v1/YOUR_TABLE?select=*&limit=1" \
  -H "apikey: YOUR_PUBLISHABLE_KEY"

先確認資料表裡有帳號 A 建立的測試資料。未登入者或帳號 B 不應讀到它,A 則應能正常讀取。以下範例假設 notes.user_id 存放作者的使用者 ID,只開放本人讀取;已有其他 policy 時,也要一起檢查:

alter table public.notes enable row level security;

create policy "owner can read notes"
on public.notes for select
to authenticated
using ((select auth.uid()) = user_id);

公開內容可以另設適當規則,但私人筆記不能對所有人使用 USING (true)。上面的範例只示範讀取,不包含寫入功能。新增與修改需要另外設計 policy,並用 with check 檢查新資料仍屬於目前使用者。還要檢查資料表權限與其他 policy,不能只看這一條。來源:Supabase RLS 文件

Firebase

在私人資料的規則中搜尋下面兩種寫法。第二種只確認登入,在該 match 路徑涵蓋的範圍內,任何登入者都會通過,沒有區分資料擁有者:

allow read, write: if true;
allow read, write: if request.auth != null;

以下以私人筆記為例,改成比對資料擁有者,並限制可以寫入的欄位:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /notes/{noteId} {
      allow read, delete: if request.auth != null
        && resource.data.ownerId == request.auth.uid;
      allow create: if request.auth != null
        && request.resource.data.ownerId == request.auth.uid
        && request.resource.data.keys().hasOnly(['ownerId', 'text'])
        && request.resource.data.text is string;
      allow update: if request.auth != null
        && resource.data.ownerId == request.auth.uid
        && request.resource.data.ownerId == resource.data.ownerId
        && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['text'])
        && request.resource.data.text is string;
    }
  }
}

這份範例假設筆記只有 ownerId 與文字 text,更新時只能改文字,不能轉移擁有者。其他欄位要依實際資料模型另外驗證。用 Firebase Emulator 測試 A 可操作、B 與未登入者被拒絕,並測試修改 ownerId 會失敗。

這個範例已在本機 Firestore 模擬器測過 17 個案例,包括本人操作成功、其他帳號與匿名者被拒絕,以及不能改擁有者、加入額外欄位或寫入錯誤型別。它只證明這份筆記範例的行為,不代表你的整個專案都已安全。

Firebase Admin SDK 的伺服器存取會略過這些客戶端規則,API 還是要自己檢查身分與資料權限。App Check 可協助驗證請求來自你的 App,不能取代使用者權限檢查。Firebase 欄位限制、App Check 說明

3. API 有沒有確認資料屬於目前使用者?

這一項很容易漏掉,因為功能測試通常只用一個帳號。

// 示意:requireLogin 必須在伺服器驗證登入憑證
const session = await requireLogin();
// 查詢條件同時限制訂單 ID 與目前登入者
const order = await db.order.findFirst({
  where: { id: params.id, userId: session.user.id },
});

用兩個帳號測試:

  1. 用帳號 A 建立一筆私人資料,記下網址或 API 請求裡的 ID。
  2. 登入帳號 B,把請求裡的 ID 換成 A 的 ID。
  3. 對測試資料分別嘗試讀取、修改與刪除。API 可回 403 或 404;重點是不能回傳私人內容,也不能成功改動資料。
  4. 把請求內容裡的 userId、role、price 等欄位改掉再送一次,確認伺服器不採用用戶端傳來的值。

4. 上傳的檔案是不是誰都看得到?

證件、合約、私人照片不要放在公開 bucket。依儲存服務設定私人存取,透過權限規則或伺服器驗證身分後提供檔案。若使用簽署網址,要設定期限,並知道拿到網址的人通常就能在期限內使用它;檔名難猜本身不算保護。

上傳端也要限制檔案類型與大小,而且限制要做在伺服器或 Storage 規則,只檢查前端沒有效果。Supabase bucket 存取方式、Firebase Storage 規則

5. 付款狀態是不是由伺服器驗證後才更新?

用戶付款後跳回「成功頁」並不代表錢真的收到了,不能由前端傳來的參數決定開通。

Stripe 文件指出,沒有驗證 webhook 時,偽造的事件可能觸發出貨或開通。檢查四件事:

  • 用 Stripe-Signature 與原始 request body 驗證簽章。
  • 同一事件可能重複送達,事件 ID 的去重紀錄與開通動作要防止同時執行兩次。也要避免不同事件重複開通同一筆訂單。
  • 開通依據是驗證過的事件,金額和方案也要用伺服器端資料比對。
  • 要非同步處理時,先驗證並把事件可靠地存入佇列,再回應 2xx。不能回應成功後只在記憶體中繼續執行,否則程序中斷可能漏單。

Stripe 提醒事件可能重複,也不保證順序。除了測試正常付款,還要重送事件並打亂順序,確認不會重複開通或把新狀態改回舊狀態。來源:Stripe webhook 文件

使用其他金流時,按照該服務的官方文件驗證回傳;不要直接套用 Stripe 的欄位或簽章算法。

6. 有沒有限制濫用和帳單金額?

至少對這些端點加上限制:登入、註冊、寄信、重設密碼、呼叫 AI API、產生檔案。

串接 AI API 的服務還要多做三件事:每位使用者設每日額度、限制單次輸入長度,並設定全站預算與停止呼叫的條件。服務商的費用警示不一定會停止請求,帳務資料也可能延遲;要確認警示和硬性上限的差別,不能只靠收到信才處理。

7. Git 歷史與部署環境有沒有洩漏?

# 查 .env 有沒有被提交過
git log --all --full-history --oneline -- .env .env.local

# 查目前的程式碼
git grep -lE 'sk_live_|sk-proj-|sk-ant-|sb_secret_|service_role'
  • 把含秘密的 .env、.env.local 與服務帳戶 JSON 加入 .gitignore。只有假值的 .env.example 可以追蹤;加入忽略規則不會移除已提交的檔案。
  • 檢查 GitHub 的 secret scanning 與 push protection 設定,確認你的 repo 與方案實際支援哪些保護。Push protection 只能阻擋它辨識的秘密格式,不是所有機密資料都會被抓到。GitHub 說明
  • 金鑰進過 Git 歷史,就算刪掉 commit 也當作已外洩,要撤銷並換新。
  • 測試環境和正式環境用不同的金鑰與資料庫。

8. 出事時你知道怎麼處理嗎?

上線前先把這些準備好:

  • 確認資料庫有備份,並在測試環境實際還原過一次。
  • 記錄登入、權限變更與管理操作,但不要把密碼、token 或完整個資寫進 log。
  • 列出各項金鑰的管理後台,出事時能快速撤銷或更換。清單只寫用途與位置,不抄金鑰。
  • 預先決定出事時的聯絡與公告管道,以及誰負責確認受影響範圍。

若發現外洩,先限制存取、撤銷受影響的金鑰,並保留必要的事件紀錄。接著確認受影響資料與使用者範圍,再依適用法規處理通知與通報;不要在尚未釐清範圍時宣稱「沒有影響」。

請 AI 幫你檢查時可以怎麼問?

請審查這個專案上線前的權限與機密資料問題。只列出你在程式碼中實際看到的問題,並附上檔案路徑與行號,不要修改程式碼。

1. 列出所有 API route、server action 與資料表,標明誰可以讀、誰可以寫、伺服器端在哪一行檢查。
2. 找出只檢查「有登入」,沒有檢查「資料屬於目前使用者」的地方。
3. 找出會被打包到前端的環境變數與金鑰。
4. 列出沒有 RLS 或 Security Rules 保護的資料表與 Storage bucket。
5. 找出沒有驗證簽章的 webhook,以及沒有額度限制的 AI API 呼叫。
6. 說明每個問題的攻擊方式,以及我可以用什麼步驟自己驗證。

AI 的審查可以幫你找出該看的地方,但不能取代實際測試。第 2 項和第 3 項的兩個帳號測試,請自己跑過一次。

先安排一小時找出高風險問題

這一小時是初步排查,發現問題後仍要修正與重測,不是時間到了就能上線。

  1. 10 分鐘,搜尋前端與 Git 的金鑰。 跑第 1 項和第 7 項的指令,發現問題就換金鑰。
  2. 20 分鐘,檢查每張資料表。 Supabase 看 rowsecurity,Firebase 看規則,並用未登入狀態測一次。
  3. 20 分鐘,跑兩個帳號測試。 從最敏感的資料開始,例如訂單、訊息與個人資料。
  4. 10 分鐘,檢查限制是否生效。 驗證超過額度後確實停止 AI 呼叫,並確認寄信與登入的頻率限制。還沒實作的項目另列修正工作。

來源、驗證範圍與下一步

文中連結指向各服務的官方文件。另可參考 Firebase 常見不安全規則與本機規則測試方法。

Supabase 新專案優先使用 publishable/secret keys。舊教學中的 anon/service_role 是先前的金鑰類型,遷移方式以官方說明為準。

本文以 AI 協助整理檢查項目與示範,官方來源最後查核於 2026 年 11 月 14 日。Firestore 筆記範例在同日通過本機模擬器的 17 項權限檢查;其他範例用來說明檢查方式,沒有代替你對實際專案的測試。

先從最敏感的一筆資料開始,用 A、B 和未登入狀態各測一次。修好後再跑一次,確認真正改變的是存取結果,不只是畫面上的提示。

繼續閱讀

有新文章時寄給你

新文章、新上架的作品和活動消息,有東西才寄。不用註冊,隨時可以退訂。

訂閱電子報
所有文章

你也有想分享的事?任何人都可以投稿文章。