昨天,CI 全綠了:lint、型別、單元測試、E2E,都通過了。
至少照著我預期的方式使用,網站現在是正常的。搜尋找得到專輯、網址打得開、權限也照規則運作。
但網站上線之後,來的人不會只照著我設計好的路走。有人會改網址、改參數、直接送 API 請求,甚至故意去試那些畫面上根本沒有提供的操作。這時候,「功能正常」就不夠了。還要知道:當有人不照規則使用時,我原本設下的限制還擋不擋得住?
這就是今天要看的資安問題。今天的文章將介紹一些開發網站時,可能會不小心忽視的安全問題,以及如何透過 AI 來輔助我們找出問題並修改。
用這個系列曾用過的餐廳來比喻:
| 功能測試問的 | 資安問的 |
|---|---|
| 該看的資料,看得到嗎? | 不該看的資料,看得到嗎? |
| 按鈕按下去,能不能正常送出? | 別人能不能替你按下去? |
測試驗證的通常是「預期行為」;資安思考的是「非預期行為」。
資安測試也可以放進 CI,例如昨天測試的其中一條「訪客讀不到錯誤回報」的權限測試就是其中一個例子;差別在於思考的方向。
攻擊網站的人,大多不是衝著你來的。他們會用程式自動掃過大量網站,哪裡有洞就往哪裡咬,就像鯊魚聞到血腥味。下面是幾種最常見的,最後一欄是這次針對專案盤點的結果。
✅ 只代表「這次盤點沒找到可利用的問題」,不是保證安全;⚠️ 代表找到了,需要修或做出決定。
| 攻擊 | 一句話 | 比喻 | 專案目前的情況 |
|---|---|---|---|
| 越權存取 | 改一下網址或參數,就看到別人的資料 | 飯店房卡改個號碼,就打開別人的房間 | ✅ 每張表都有 RLS;收藏只能讀寫自己的,後台在伺服器和資料庫各檢查一次管理員身分 |
| XSS(跨站腳本) | 把程式碼藏在內容裡,讓別人的瀏覽器執行 | 意見箱裡投的不是字條,是一張會動手的紙條 | ✅ React 預設把內容當文字顯示;專案裡沒有直接插入 HTML 的寫法,外部連結也限制只能是 https:// |
| CSRF(跨站請求偽造) | 借用你已登入的身分,偷偷送出請求 | 有人拿你簽好名的空白單據去填 | ✅ Server Actions 會比對請求來源;唯一自己寫的 API 也有檢查來源 |
| SQL injection | 輸入的內容被當成資料庫指令執行 | 在表單姓名欄寫「順便把全部資料給我」 | ✅ 程式透過 Supabase 傳值,沒有自己拼 SQL;資料庫函式裡也沒有把輸入串進 SQL 指令 |
| 濫用 | 不停重試,或大量送出 | 一直試密碼鎖,或塞爆意見箱 | ⚠️ 錯誤回報有頻率限制,但同時送出大量請求時可以超過上限(見 5.2) |
| 開放重導向 | 讓你的網站,幫忙把人送到釣魚網站 | 用你家的門牌當指路牌,指向別人的陷阱 | ⚠️ 登入後的跳轉可以被繞過(見 5.1) |
這張表有兩件事值得注意:
其他常見的漏洞,這篇先不展開討論:
pull_request_target 權限較高、可以讀 Secrets,如果又去執行外部 PR 的程式碼,攻擊者就可能碰到它們。Day 25 用的是 pull_request。Claude Code 內建的 /security-review,預設只檢查目前分支改了什麼。我第一次執行時它只看了三行,回報「沒有發現漏洞」。所以第一次導入時,要明確要求掃描整個專案。例如,我使用下列的同一份指令,分別交給 Claude Code 和 Codex:
請對整個專案做一次資安盤點(不只是目前分支的變更)。
只回報,不要修改任何檔案,也不要讀取或輸出 .env 的內容。
重點檢查:越權存取(RLS、伺服器端的身分檢查)、XSS、CSRF、
SQL injection(字串組 SQL)、開放重導向(登入後跳轉)、
金鑰(NEXT_PUBLIC_ 是否只有可公開的值)、濫用(頻率限制)。
每個發現請列出:嚴重程度、信心程度、檔案與行號、攻擊情境、建議修法。
不確定的請標註「需人工確認」,不要為了填滿報告而列出低信心的項目。
除了自己寫指令,也可以替 AI 安裝資安 skill。skill 不是掃描引擎,而是一份寫給 AI 看的說明書:要看哪些地方、怎麼判斷是不是真的漏洞、報告怎麼寫。真正讀程式碼的,還是 AI 本身。例如:security-best-practices、security-threat-model 、security-review 等等。
不過,裝別人的 skill 之前,要先讀過內容。 skill 是會被 AI 照著做的指令,安裝來路不明的 skill,等於讓陌生人指揮你的 AI;而且 skill 更新之後,內容也可能跟當初不一樣。這跟安裝 npm 套件是同一個問題:你沒寫的東西,一樣會進到你的專案裡。
| 項目 | Claude Code | Codex | 結果 |
|---|---|---|---|
| 開放重導向 | ⚠️ 找到繞過方法,並實際執行驗證 | ✅ 認為安全 | 真的漏洞 |
| 回報的頻率限制 | ⚠️ 同時送出可以超過上限(低) | ⚠️ 同一個問題(中) | 兩邊都找到,嚴重程度判斷不同 |
| 偽造來源 IP | 補充:Vercel 會覆寫,用戶端無法偽造 | ⚠️ 中,需人工確認 | 在 Vercel 上不成立,Vercel 文件寫明會覆寫這個標頭,防止偽造 |
資安檢查有兩種常見的錯誤:漏報,真的有洞卻沒看到;誤報,看起來像洞,其實利用不了。這次兩種都遇到了:
登入頁可以帶一個 next 參數,記住使用者原本在哪一頁,登入後跳回去。為了不被拿來跳到外部網站,程式會先擋掉 // 開頭的網址(瀏覽器會把 //evil.com 當成 https://evil.com),再把網址整理一遍。
問題就在順序。整理時,/./ 這種「目前資料夾」的寫法會被拿掉,所以 /.//evil.com 檢查時不是 // 開頭,順利通過,整理完卻變成 //evil.com。/..//evil.com、/%2e//evil.com 也一樣。

【圖 1|開放重導向的攻擊流程】
修法是把順序反過來:先整理,再檢查,確認整理後的網址仍然在自己的網域。修好之後,把這三種寫法寫成測試放進 CI。
每個真的漏洞,修好之後,都盡量留下一條測試。
錯誤回報有限制:同一個來源,每小時最多 5 筆。程式的做法是:
先查:最近一小時送了幾筆? → 如果少於 5 筆 → 再寫入
一筆一筆送,這樣沒問題。但如果「同時」送出 50 筆,每一筆在查詢時都看到「目前 0 筆」,於是全部寫入成功,管理員的 Telegram 也會收到 50 則通知。
這種「先查再寫,中間被插隊」的問題,叫做競態條件(race condition)。就像兩個人同時看到電影院剩最後一個位子,都按下購買,結果同一個位子賣了兩次。本的修法,是把限制交給資料庫保證,例如鎖、唯一限制,或設計好的交易(transaction),而不是只靠程式先查再寫。
這次的決定:這個問題只會造成垃圾回報和通知洗版,不會洩漏資料。所以先在門口用 Vercel 的規則擋住大量請求,程式裡的競態條件記錄下來,之後再修。
昨天 npm 回報 3 個已知漏洞,今天重跑變成 8 個,程式碼卻一行都沒改。漏洞資料庫一直在更新,所以數字會自己變。
但這 8 個追下去,只有 4 條來源:Next.js、PostCSS、sharp 和 ESLint 工具鏈,而且觸發條件大多跟 Biashelf 對不上,有一條還只在開發時使用。數字不等於風險,要看攻擊者有沒有路徑碰得到。
npm 建議的 npm audit fix --force,會把 ESLint 設定降回舊版,跟目前的 Next.js 對不上:一鍵修復可能修錯方向。這次的處理方式,是開分支升級 Next.js,讓 CI 跑過再合併。有了 CI,升級套件就比較敢做。
前面講的,都是廚房裡的規矩。網路層則是門口的警衛:擋得住擠爆門口的人群,卻看不出某位客人改了帳單。兩者不能互相取代,這就是分層防禦。Biashelf 目前的防線是這樣:

【圖 2|Biashelf 的分層防禦】
Biashelf 的流量直接送到 Vercel,所以實際站在門口的,是 Vercel 的防火牆:
| 功能 | 做什麼 | Biashelf |
|---|---|---|
| 自動 DDoS 防護 | 異常的大量流量自動擋,擋掉的不收費;被攻擊時還能開 Attack Challenge Mode,讓所有訪客先過驗證 | 所有方案都有 |
| 自訂規則 | 依路徑、IP、國家,擋下或要求驗證 | 需要時再加 |
| 頻率限制規則 | 某個路徑,每個 IP 在一段時間內最多幾次 | 這次實作 |
這次替錯誤回報加了一條頻率限制規則:
路徑:/api/data-reports,方法:POST
同一個 IP 每分鐘超過 ⚠️ 次 → 直接拒絕
程式裡「先查再寫」的漏洞還在,但同時送來的大量請求,大部分在門口就被擋掉,進不到程式裡。這是緩解,不是修好。另外,在按用量計費的平台上,大量流量更常造成的傷害是帳單:沒被認定為攻擊的流量(例如大量爬蟲)一樣會計費,記得設好用量上限的通知。
Cloudflare 的 WAF、Bot Fight Mode,要流量經過 Cloudflare 才有作用。Day 23 設的是灰雲,流量直接到 Vercel,所以開了也擋不到東西。改成橘雲可以做,但 Vercel 官方一般不建議:多繞一層會變慢,而且部分用來辨識機器人的訊號可能失真或缺失,Vercel 防火牆的偵測效果會下降。
灰雲下,Cloudflare 還是有幾樣可以用:
可以自行取捨是否要讓流量通過 Cloudflare。
這次的漏洞不是 AI 刻意寫壞的,但 Vibe Coding 有一些很常見的模式:AI 的目標是「讓錯誤消失」,不一定是「讓它安全」。
| AI 很容易做的事 | 後果 |
|---|---|
| 遇到權限錯誤或測試失敗,直接關掉 RLS、拿掉檢查 | 錯誤消失了,門也打開了 |
為了方便,直接相信前端傳來的 user_id |
越權:改一個參數就能操作別人的資料 |
把 secret key 加上 NEXT_PUBLIC_ |
金鑰被打包進前端,任何人都看得到 |
看到漏洞警告,直接 npm audit fix --force |
套件被降版,網站可能直接壞掉 |
還有一個容易忽略的:AI 本身能做什麼,也有可能是攻擊面的一部分。 Day 17 那次,AI 自己執行了 supabase db push --yes,直接改了遠端資料庫。AI 能執行的指令越多,出錯時影響的範圍就越大。
另一種新的漏洞,攻擊的是幫你寫網站的 AI:提示注入(prompt injection),把指令藏在 AI 會讀到的網頁、issue 或文件裡,讓它做你沒叫它做的事。像 Codex、Claude Code 這類會讀外部內容、又能執行指令的工具,特別要注意。
| 測試 | 預期結果 |
|---|---|
用 ?next=/.//evil.com 登入 |
停留在 Biashelf,不會跳到外部網站 |
| 開放重導向的三種寫法,寫成測試放進 CI | CI 通過 |
| 短時間內大量送出錯誤回報 | 超過 Vercel 規則的上限後被拒絕 |
升級 Next.js 後重跑 npm audit |
高風險、而且碰得到的漏洞已修補;其他的有明確的處置理由 |
| 產品 | 最怕的鯊魚 |
|---|---|
| 電商 | 改價格、讀到別人的訂單 |
| SaaS | 跨公司讀到別人的資料 |
| 教育平台 | 學生看到老師的後台 |
| 金流 | 同一筆付款請求被重送 |
技術名詞不同,但問的其實都是同一件事:誰可以做什麼?有沒有人能做到他不該做的事?
今天的新觀念:
| 觀念 | 一句話 |
|---|---|
| 越權存取 | 改一下參數,就能碰到別人的資料 |
| 開放重導向 | 你的網站,幫忙把人送去釣魚網站 |
| 競態條件 | 先查再寫,中間被插隊 |
| 漏報、誤報 | 有洞沒看到;看起來像洞,其實利用不了 |
| 分層防禦 | 程式和網路各擋一層,不靠單一道防線 |
Day 25 把「正常使用」變成每次都會檢查的規則;今天反過來問:如果有人故意不照正常方式使用呢?AI 可以當很好的資安檢查員,但它說「沒問題」的時候,要問清楚:看了哪裡?證據是什麼?攻擊條件真的成立嗎?必要時,換另一個 AI 再看一次。
網站能正常運作,也擋住了幾隻鯊魚。但如果使用者根本找不到這個網站,或者網站出了問題,我自己卻不知道呢?明天,我們來讓網站被看見,也讓自己看得見網站的流量。
我們明天見。