iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0

前端做的檢查是給使用者看的,不是給攻擊者看的。

大綱

  • 情境:付費功能的按鈕變灰了,但有人照樣在用
  • 前端程式是「下載到使用者電腦上」執行的
  • 三個常見誤會:藏按鈕、前端檢查、前端環境變數
  • 30 秒自我檢查:用瀏覽器開發者工具看自己的網站
  • 提示詞:列出所有會被送進瀏覽器的機密與只在前端做的檢查
  • 驗證:未登入重送、免費帳號重送、連續送超過上限
  • 機制:bundler 怎麼把環境變數寫進 bundle、source map
  • 「公開金鑰」:Supabase、Firebase 哪些能放前端,哪些不能
  • CORS 不是權限控制
  • token 放 localStorage 還是 HttpOnly cookie
  • 程式碼:把 AI 呼叫和額度檢查移到伺服器端
  • 測試:在 CI 掃描打包後的檔案
  • defense in depth、對應標準
  • 攔截點:該在哪個 SSDLC 階段攔下、要問的問題
  • 劃重點

【情境】

阿哲用 AI 做了一個履歷健檢網站:貼上履歷,AI 給修改建議。免費會員每天能用三次,付費會員無限次。

次數限制寫在畫面上:今天用滿三次,按鈕就變成灰色,旁邊跳出「升級付費方案」。

一週後,AI API 的帳單讓他嚇了一跳。他去翻紀錄,發現有個免費帳號一天用了四百次。

那個人沒有駭進任何系統。他打開瀏覽器內建的開發者工具,找到按下按鈕時送出的請求,複製下來,自己重送了四百次。伺服器收到請求就照做,因為「一天三次」這條規則,只存在於阿哲的畫面上。

畫面上的限制,攔住的是守規矩的人。

前端程式是「下載到使用者電腦上」執行的

第 1 天說過,服務由前端、後端、資料庫、第三方服務四塊組成,而前端在使用者手上。今天把這句話講得更具體一點。

你打開一個網站時,瀏覽器會把畫面的程式碼整份下載到你的電腦上執行。這代表每一個使用者都可以:

  • 看到這些程式碼的內容,包括裡面寫死的網址、設定和金鑰
  • 看到網站和伺服器之間傳送的每一個請求
  • 修改、複製、重送這些請求,想送幾次就送幾次
  • 看到、修改存在瀏覽器裡的資料

這些事不需要任何駭客工具。每個瀏覽器都內建「開發者工具」,按 F12(Mac 是 Cmd+Option+I)就能打開。

用昨天的五個思考模型來說,這篇只問第一個問題「這份資料從哪裡來?誰能控制它?」。答案是:從瀏覽器來的一切,都由使用者控制。trust boundary 畫在瀏覽器和伺服器之間,檢查只能做在邊界的伺服器那一側。

三個常見誤會

誤會一:把按鈕藏起來,別人就不能用。
藏起來的是按鈕,不是功能。按鈕背後的請求還在,任何人都能直接送。第 13 天會看到,管理員後台常常也是這樣「保護」的。

誤會二:在畫面上檢查過,資料就是乾淨的。
輸入格式、剩餘次數、付款金額,畫面上的檢查都可以被跳過。伺服器收到的每一筆資料,都要當成「沒檢查過」。前端檢查仍然有價值,它讓正常使用者早點看到錯誤訊息,但它是使用者體驗,不是安全措施

誤會三:金鑰放在環境變數裡,就是藏起來了。
要看是哪一種環境變數。很多框架規定,名稱開頭是 NEXT_PUBLIC_VITE_ 的變數,會被直接寫進送給瀏覽器的程式碼。AI 為了讓功能快點動起來,有時會直接從前端呼叫 AI 服務,把 API key 放進這種變數裡。

這不是少數案例。資安公司 Escape 在 2025 年 10 月掃描了 5,600 多個用 Lovable、Base44、Bolt.new 等工具做出來、已經公開上線的應用程式,找到 400 多個外洩的機密(API key、access token 等),研究報告特別提到其中一個主要來源是前端的 JavaScript bundle。

30 秒自我檢查

用 Chrome 打開你的網站,先登入,把主要的頁面都點過一輪(有些程式碼要打開那一頁才會下載),然後按 F12:

  1. 原始碼(Sources)分頁:按 Ctrl+Shift+F(Mac 是 Cmd+Option+F)搜尋整個網站的程式碼。依序搜尋:

    • sk-sk_livesb_secret:常見的 AI 服務、金流、Supabase secret key 開頭
    • eyJ:JWT 都是這樣開頭,Supabase 的舊版金鑰也是(下面「公開金鑰」一節會教你怎麼判斷它是哪一種)
    • secretapiKeypassword

    搜得到的,全世界都搜得到。

  2. 網路(Network)分頁:操作一次付費功能或有次數限制的功能,看送出了哪個請求、送到哪個網址。如果請求直接送到 api.openai.com 這類第三方服務,代表金鑰一定在瀏覽器裡。

  3. 應用程式(Application)分頁:看左側「本機儲存空間(Local Storage)」和「Cookie」裡存了什麼。有沒有 token?有沒有 isPremium: trueremaining: 3 這種看起來可以自己改的值?

(括號內是英文介面的名稱。)

貼給 AI 的提示詞

請檢查這個專案,列出兩份清單,先不要修改任何程式碼:

清單一:所有會被打包進瀏覽器端程式碼的環境變數、常數和金鑰。
每一項標出它是不是機密:能產生費用、能讀寫所有資料、能冒充伺服器的,都算機密。
如果是 Supabase 或 Firebase 的金鑰,說明它是設計上可以公開的那一種,還是不能公開的那一種。

清單二:所有「只在前端檢查」的權限或限制。
例如付費功能、使用次數、管理員功能、輸入格式、價格。
每一項說明伺服器端有沒有做同樣的檢查,附上前端和伺服器端的檔案位置;伺服器端沒有檢查的,明確寫「沒有」。

清單出來之後,請 AI 修正時,把這兩句規則一起給它:

機密只能在伺服器端使用,不能出現在任何會送到瀏覽器的程式碼裡。
所有權限和限制都要在伺服器端檢查;前端的檢查只負責顯示,可以保留,但不能是唯一的一道。

已經被打包進前端的金鑰,改完程式碼還不算修好。它早就公開過了,要到服務後台撤銷、換一把新的(第 28 天會完整談)。

怎麼確認真的修好了

三個測試都在瀏覽器裡做,不用看程式碼。

**第一步:複製請求。**用付費帳號操作一次功能,在網路分頁找到那個請求,按右鍵 →「複製」→「複製為 fetch 格式」(英文介面是 Copy → Copy as fetch)。

**第二步:換個身分重送。**把剛剛複製的內容,貼到下面三種情境的「主控台(Console)」分頁執行。第一次貼上時 Chrome 會擋下來,照畫面指示輸入「允許貼上」(英文介面是 allow pasting)再貼一次。

測試 怎麼做 應該看到
未登入 開無痕視窗,打開你的網站但不登入,在主控台貼上請求。如果內容裡有 "authorization" 那一行,先把它刪掉 狀態碼 401 或 403
免費帳號 用免費帳號登入,貼上付費帳號複製來的請求。如果內容裡有 "authorization" 那一行,把它的值換成免費帳號的(免費帳號送出的任何一個請求裡都看得到) 狀態碼 403
超過次數 用免費帳號登入,把請求包在迴圈裡連送 5 次(見下方) 前 3 次成功,之後 429 或 403

超過次數的測試,把複製來的 fetch(...) 整段貼進下面的迴圈:

for (let i = 1; i <= 5; i++) {
  const res = await fetch(/* 把複製來的 fetch 括號裡的內容貼在這裡 */);
  console.log(i, res.status);
}

第三步:重新做一次 30 秒自我檢查的搜尋,機密應該搜不到了。記得是對重新部署之後的網站搜尋。

狀態碼在主控台看得到,也可以回到網路分頁看「狀態」欄。三個測試只要有一個回應 200 而且真的產生了結果,就代表伺服器沒有擋。

機制:環境變數怎麼跑進 bundle

前端程式碼上線前,會經過框架或建置工具裡的 bundler 打包成幾個 JavaScript 檔,這些檔案叫 bundle。打包時,bundler 會把特定前綴的環境變數,用字串常數直接替換進程式碼

工具 會送進瀏覽器的前綴 production 預設產生 source map 嗎
Next.js NEXT_PUBLIC_ 不會(productionBrowserSourceMaps 預設關閉)
Vite VITE_ 不會(build.sourcemap 預設 false
Create React App REACT_APP_ (要設 GENERATE_SOURCEMAP=false 才關)

以 Next.js 為例,你寫的是:

fetch(url, { headers: { Authorization: `Bearer ${process.env.NEXT_PUBLIC_OPENAI_KEY}` } });

使用者下載到的是:

fetch(url, { headers: { Authorization: `Bearer sk-proj-xxxxxxxx` } });

幾個容易被忽略的細節:

  • 替換發生在 build 的時候。從 .env 刪掉變數,已經部署的 bundle 不會變;很多部署平台還會保留舊版本的網址。所以外洩過的金鑰只能撤銷,不能靠刪除。
  • 變數名稱不用 NEXT_PUBLIC_ 也可能外洩。只要一段程式碼在瀏覽器執行,它用到的任何值最後都在使用者手上。例如在 Next.js 的 client component(檔案開頭有 "use client")裡寫死金鑰字串。
  • source map 是讓除錯工具把壓縮後的程式碼還原成原始碼的對照檔。production 如果把它公開,使用者看到的就不只是壓縮過的程式碼,而是你的原始檔案結構、變數名稱和註解。

把應該在伺服器端做的安全檢查放到前端,在 CWE 叫做 CWE-602:Client-Side Enforcement of Server-Side Security;把金鑰寫死在程式碼裡,是 CWE-798:Use of Hard-coded Credentials

「公開金鑰」:哪些能放前端

用 AI 開發時最常見的後端服務 Supabase 和 Firebase,設計上就是讓前端直接連線,所以前端一定會有一把金鑰。重點不是「有沒有金鑰」,而是「是哪一種金鑰」。

平台 可以放前端 絕對不能放前端 真正保護資料的是
Supabase(新版) publishable key(sb_publishable_... secret key(sb_secret_... Row Level Security
Supabase(舊版) anon key service_role key Row Level Security
Firebase Firebase 設定裡的 apiKeyAIza... 同一個 Google Cloud 專案裡,用來呼叫 Gemini API 等付費服務的金鑰 Firebase Security Rules、App Check

Supabase 的 secret key 和 service_role key 會繞過所有 Row Level Security policy,拿到它就等於拿到整個資料庫。Supabase 對新版 secret key 加了一道保險:偵測到請求來自瀏覽器(依 User-Agent 判斷)時直接回 401。舊版 service_role key 沒有這道保險,而 Supabase 預計在 2026 年底前淘汰 anonservice_role 這兩種舊金鑰。

舊版金鑰都是 eyJ 開頭的 JWT,外表長得一模一樣。要分辨是哪一種,在自己網站的主控台執行下面這段(不要貼到線上的 JWT 解碼網站,那等於把金鑰交給別人):

const token = "貼上 eyJ 開頭的整串";
JSON.parse(atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));

結果裡的 roleanon 沒問題;是 service_role 就要立刻撤銷。

Firebase 的 apiKey 則有一段值得知道的插曲。Firebase 官方文件一直說,限制在 Firebase 服務的 API key 只是用來識別專案,不需要當成機密。但 Truffle Security 在 2026 年 2 月揭露:在同一個 Google Cloud 專案啟用 Gemini API 後,專案裡原本就公開在網頁上的 API key 會悄悄多出呼叫 Gemini 的能力。他們在 2025 年 11 月的 Common Crawl 公開網頁資料裡,找到 2,863 把受影響、還能用的金鑰。Google 之後改成用新的 auth key 呼叫 Gemini,並從 2026 年 6 月起拒絕沒有設限制的舊式 API key。

這件事的教訓是:「這把金鑰可以公開」是有前提的,前提是它的權限範圍沒有變。所以要定期檢查:前端那把金鑰,現在能呼叫哪些服務?

另外,「金鑰可以公開」的意思是資料的保護責任全部落在 Row Level Security 或 Security Rules 上。第 1 天的 Lovable 事件(CVE-2025-48757),就是 anon key 本來可以公開,但 Row Level Security 沒開。這是第 5 天的主題。

CORS 不是權限控制

AI 很常在遇到跨網域錯誤時「修好」CORS,於是很多人以為 CORS 設定就是權限控制:「我只允許自己的網域,別人就不能呼叫我的 API。」

CORS(Cross-Origin Resource Sharing)限制的是:瀏覽器裡、另一個網站的 JavaScript,能不能讀取你 API 的回應。它保護的是使用者的瀏覽器,不是你的伺服器。它管不到:

  • curl、Postman、任何腳本直接呼叫你的 API,這些工具根本不看 CORS
  • 你自己網站上的主控台,前面「怎麼確認真的修好了」就是這樣重送請求的
  • simple request 還是會送到伺服器

最後一點我自己踩過。以前做前後端分離的專案,前端在 www 網域、API 在 api 網域,我發現同樣是 POST,用 application/json 會先送一個 OPTIONS 的 preflight request,用 application/x-www-form-urlencoded 卻直接送出去了。

原因是符合條件的請求(GET、HEAD、POST,Content-Type 只用 text/plainmultipart/form-dataapplication/x-www-form-urlencoded,而且沒有自訂標頭)屬於 simple request,瀏覽器不做 preflight,直接把請求送到伺服器。伺服器照常執行,瀏覽器只是在回應回來之後,不讓頁面讀取結果。如果那個請求是「刪除資料」或「扣款」,事情已經發生了。

所以 CORS 設定要正確,但它永遠不能取代伺服器端的身分與權限檢查。

token 放哪裡

登入之後,瀏覽器要記住「你是誰」,通常是存一個 token。AI 生成的前後端分離專案,最常見的寫法是把 token 存在 localStorage,每次呼叫 API 放進 Authorization 標頭,因為最好寫。

存放位置 JavaScript 讀得到嗎 主要風險
localStorage/sessionStorage 讀得到 任何一個 XSS 漏洞就能把 token 讀出來、送到別的地方,攻擊者在自己的電腦上繼續使用
HttpOnly cookie 讀不到 瀏覽器會自動附上 cookie,要處理 CSRF(SameSite、CSRF token)

OWASP 的 HTML5 Security Cheat Sheet 寫得很直接:不要把 session identifier 存在 local storage,因為 JavaScript 永遠讀得到。

但也別把 HttpOnly cookie 當成萬靈丹。XSS 發生時,攻擊者偷不走 token,還是可以趁使用者開著網頁,用使用者的身分送請求。差別在於傷害被限制在那個頁面開著的時間內,而且 token 不會流出去被重複使用。

所以真正的結論是:

  • 用 localStorage 不是絕對錯,但要知道從此以後,任何一個 XSS 漏洞都等於帳號被接管
  • 能用平台或框架內建的 cookie-based session,就不要讓 AI 自己發明一套
  • 不管存哪裡,XSS 本身都要防(第 16 天)

登入與 session 的細節,第 14 天會再深入。

程式碼:把 AI 呼叫和額度檢查移到伺服器端

以 Next.js 為例。錯誤寫法:金鑰進了 bundle,次數限制只在畫面上。

// app/resume/page.tsx
"use client";

export default function ResumePage() {
  const [remaining, setRemaining] = useState(3); // quota lives in the browser

  async function review(text: string) {
    if (remaining <= 0) return; // client-side only check
    const res = await fetch("https://api.openai.com/v1/chat/completions", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.NEXT_PUBLIC_OPENAI_KEY}`, // inlined into the bundle
      },
      body: JSON.stringify({ model: "gpt-4o-mini", messages: [{ role: "user", content: text }] }),
    });
    setRemaining(remaining - 1);
    // ...
  }
  // ...
}

修正方向:瀏覽器只呼叫自己的 API;身分、額度、金鑰都留在伺服器。

// app/api/resume-review/route.ts
import "server-only"; // build fails if this module is imported from client code

export async function POST(req: Request) {
  const user = await getCurrentUser(req); // read the session on the server
  if (!user) return new Response("Unauthorized", { status: 401 });

  // Atomic check-and-increment in the database, so parallel requests
  // cannot all pass the same "remaining > 0" check.
  const allowed = await consumeDailyQuota(user.id, user.plan === "pro" ? Infinity : 3);
  if (!allowed) return new Response("Daily limit reached", { status: 429 });

  const { text } = await req.json();
  if (typeof text !== "string" || text.length > 20_000) {
    return new Response("Invalid input", { status: 400 });
  }

  const res = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, // no NEXT_PUBLIC_ prefix
    },
    body: JSON.stringify({ model: "gpt-4o-mini", messages: [{ role: "user", content: text }] }),
  });
  return Response.json(await res.json());
}

前端只剩:

const res = await fetch("/api/resume-review", { method: "POST", body: JSON.stringify({ text }) });
if (res.status === 429) showUpgradeDialog(); // UI only; the server already enforced it

三個重點:

  1. 瀏覽器永遠不直接呼叫會產生費用的第三方服務。中間一定隔著你自己的 API。
  2. 額度要「檢查並扣除」一次完成。先查剩幾次、再呼叫 AI、最後才扣次數,同時送 20 個請求就可能 20 個都通過。這是 race condition,也是第 2 天 ASVS 2.3.4 在講的問題,第 4 天會再遇到。
  3. import "server-only" 讓這個檔案一旦被 client component 引用,build 就失敗。這是把「機密只能在伺服器端」從口頭規則變成工具會擋的規則。

getCurrentUserconsumeDailyQuota 依你用的登入機制和資料庫實作,這裡只示範它們該在伺服器的哪個位置被呼叫。

測試:在 CI 掃描打包後的檔案

原始碼掃描抓不到「build 時才被替換進去」的值,所以要在 build 之後,掃描真正要部署的產物。

下面這支腳本做兩件事:搜尋已知格式的機密字串,以及把 bundle 裡每個 JWT 解碼、找出 service_role。找到就回傳 exit code 1,讓 build 失敗,這次部署就停下來。

第一步:把腳本放進專案。在專案根目錄(和 package.json 同一層)建立 scripts 資料夾,新增 scan-client-bundle.sh,內容如下:

#!/usr/bin/env bash
# scripts/scan-client-bundle.sh
set -euo pipefail

BUNDLE_DIR="${1:-.next/static}"   # Vite: dist, CRA: build

# Known secret key prefixes: OpenAI / Anthropic, Stripe live, Supabase secret
# -l prints file names only: minified bundles are one huge line, and printing it would leak the key into CI logs
if grep -rEl 'sk-(proj|ant|svcacct|admin)?-?[A-Za-z0-9_-]{20,}|sk_live_[A-Za-z0-9]{20,}|sb_secret_[A-Za-z0-9_-]{10,}' "$BUNDLE_DIR"; then
  echo "Secret-like string found in client bundle" >&2
  exit 1
fi

# Legacy Supabase keys are JWTs: decode each payload and fail on service_role
node -e '
const fs = require("fs"), path = require("path");
const walk = d => fs.readdirSync(d, { withFileTypes: true })
  .flatMap(e => e.isDirectory() ? walk(path.join(d, e.name)) : [path.join(d, e.name)]);
let found = false;
for (const file of walk(process.argv[1]).filter(f => /\.(js|mjs|map)$/.test(f))) {
  for (const jwt of fs.readFileSync(file, "utf8").match(/eyJ[\w-]+\.eyJ[\w-]+\.[\w-]+/g) ?? []) {
    try {
      const payload = JSON.parse(Buffer.from(jwt.split(".")[1], "base64url").toString());
      if (payload.role === "service_role") { console.error(`service_role JWT in ${file}`); found = true; }
    } catch {}
  }
}
process.exit(found ? 1 : 0);
' "$BUNDLE_DIR"

注意 service_role 這幾個字在 JWT 裡是 base64 編碼過的,直接 grep service_role 搜不到真正的金鑰,所以要先解碼。

第二步:在自己電腦上跑一次。在專案根目錄的終端機執行(Windows 請用 Git Bash 或 WSL):

npm run build
bash scripts/scan-client-bundle.sh .next/static

最後那個參數是「打包後、要送給瀏覽器的檔案」所在的資料夾,依工具不同:

工具 參數
Next.js .next/static.next/server 是伺服器端程式,不會送到瀏覽器,不用掃)
Vite dist
Create React App build

沒有找到問題時,畫面上什麼都不會出現。想確認的話,接著執行 echo $?,看到 0 就是通過。找到問題時會印出檔案路徑,例如:

.next/static/chunks/app/resume/page-1a2b3c.js
Secret-like string found in client bundle

service_role JWT in .next/static/chunks/4f5e6d.js

第三步:讓每次部署都自動跑。只在自己電腦跑一次,下次 AI 改程式碼時又可能漏掉。以下兩種放法選一種:

做法 A:接在 build 指令後面(用 Vercel、Netlify、Cloudflare Pages 這類平台自動部署的,就建議用這個)

打開 package.json,把 scripts 裡的 build 改成:

{
  "scripts": {
    "build": "next build && bash scripts/scan-client-bundle.sh .next/static"
  }
}

(Vite 是 vite build && bash scripts/scan-client-bundle.sh dist,依此類推。)

部署平台每次都會執行 npm run build&& 的意思是「前面成功才執行後面」,掃描失敗整個 build 就算失敗,平台不會部署這個版本,網站繼續跑上一個版本。這個做法的好處是:平台 build 時用的是 production 的環境變數,掃到的就是真正會上線的那份 bundle。

做法 B:放進 GitHub Actions(想在合併 PR 之前就擋下來)

在專案裡新增 .github/workflows/scan-client-bundle.yml

name: Scan client bundle
on: [pull_request, push]

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run build
        env:
          # Same public variable names as production, so the build inlines them the same way
          NEXT_PUBLIC_SUPABASE_URL: ${{ vars.NEXT_PUBLIC_SUPABASE_URL }}
          NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ vars.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
      - run: bash scripts/scan-client-bundle.sh .next/static

這裡有個陷阱:.env 通常不會 commit 進 git,GitHub Actions 裡預設沒有任何環境變數。build 出來的 bundle 裡金鑰都是空的,掃描永遠通過,等於沒掃。所以前端用到的每個 NEXT_PUBLIC_ 變數,都要到 GitHub repo 的 Settings → Secrets and variables → Actions 設定,再像上面的 env: 一樣傳進 build。如果覺得麻煩,用做法 A 就好。

也可以直接請 AI 幫你裝:

請在這個專案加入 scripts/scan-client-bundle.sh(內容如下),
並修改 package.json 的 build 指令,在打包完成後執行它。
依這個專案使用的框架,決定要掃描的 bundle 資料夾。
改完之後執行一次 npm run build,把掃描結果貼給我。

(貼上腳本內容)

掃到了怎麼辦?

  1. 檔名是 bundler 加上的雜湊值,看不出是哪個原始檔。回到前面的提示詞,請 AI 列出清單一,通常就能找到來源。
  2. 照「把 AI 呼叫和額度檢查移到伺服器端」那一節修正。
  3. 先去服務後台撤銷那把金鑰,再部署新版本。它有可能已經在之前的版本上線過了。

這支腳本只抓得到已知格式的金鑰,格式不在清單上的就會漏掉。正式做法是 secret scanning 工具(第 20 天放進 CI,第 28 天談外洩後的處理);伺服器端的權限,則要用第 19 天的方法寫成直接打 API 的 regression test,例如:

test("free user cannot exceed daily quota via direct API calls", async () => {
  const cookie = await loginAs("free-user@example.test");
  const statuses = [];
  for (let i = 0; i < 5; i++) {
    const res = await fetch(`${BASE_URL}/api/resume-review`, {
      method: "POST",
      headers: { cookie, "Content-Type": "application/json" },
      body: JSON.stringify({ text: "test" }),
    });
    statuses.push(res.status);
  }
  expect(statuses.slice(3)).toEqual([429, 429]);
});

defense in depth

  • production 不公開 source map;要給錯誤追蹤服務用,就只上傳到該服務,不跟著網站部署
  • server-only 這類機制,讓機密模組不可能被打包進前端
  • 第三方金鑰在服務後台設定用量上限、費用警示,Google 的 API key 設定可以呼叫哪些 API 和來源限制(第 27 天)
  • 所有前端限制都在 API 層再實作一次,並寫測試直接打 API 驗證
  • 不要讓前端送來的欄位決定權限,例如 isPremiumroleprice(第 13、15 天)

對應標準

  • CWE-602:Client-Side Enforcement of Server-Side Security,在 OWASP Top 10:2025 歸在 A06 Insecure Design
  • CWE-798:Use of Hard-coded Credentials,在 OWASP Top 10:2025 歸在 A07 Authentication Failures
  • OWASP Top 10:2025 A01 Broken Access Control:只藏按鈕、沒在伺服器檢查權限,最後的結果就是這一類
  • OWASP HTML5 Security Cheat Sheet:Local Storage 一節

【攔截點】

  • 最早該在這裡攔下:需求與設計。畫資料流時標出 trust boundary:哪些邏輯在瀏覽器執行、哪些在伺服器執行;在 threat model 裡寫明「機密只存在伺服器端」「所有限制在伺服器端檢查」。
  • 漏掉時的下一道網:驗證(第 19 天用 Copy as fetch 重送請求、寫成 regression test;第 20 天在 CI 掃描 bundle);發布(第 25 天上線檢查,再搜一次前端程式碼)。
  • 要問的問題(思考模型:信任邊界):這個檢查、這把金鑰,是在誰的電腦上執行?

【劃重點】

前端做的檢查是給使用者看的,不是給攻擊者看的。瀏覽器裡的每一行程式碼、每一個請求、每一把金鑰都是公開的;門鎖只能裝在伺服器上。

參考資料


上一篇
D02 動手之前,先問這個功能會被怎麼濫用
下一篇
D04 登入了,不代表有權限
系列文
AI 寫的程式,安全嗎?完工不是結束,是攻擊倒數的起點5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言