iT邦幫忙

2026 iThome 鐵人賽

DAY 1
0
AI Security

你該防的不是駭客,是你自己的 AI:在本機驗證你的防線系列 第 1

Day 1|AI 三十秒幫你串好的 API,金鑰在誰手上 ?

  • 分享至 

  • xImage
  •  

嗨嗨~ 大家好,我是中義。

這是三十天挑戰的第一天,現在透過 AI 來協助開發已經是常態了,從vibe coder 到資深工程師,透過 AI Agent 來協助開發已經是稀鬆平常的事情,AI 輸出的很快,但他真的安全嗎 ?

一般的資安文章在教你防外面的人,這個系列不是。這三十天要防的,是你每天在用的那些 AI 工具,還有它們幫你寫進專案裡的東西。

那不是同一件事。外面的攻擊者要先找到你,你的 AI 工具已經在你的專案裡了,它幫你寫的 CRUD 沒檢查權限、它推薦的套件可能根本不存在、你裝的那幾個 MCP 拿到了什麼權限你說不出來,這些東西沒有人會幫你看,因為你就是那個要看的人。

三十天結束的時候,你手上會有這些東西:一個不再持有金鑰的前端、一份自己專案的「什麼不能貼給 AI」清單、一組固定的攻擊集、接進 CI 的回歸測試,還有一個跑在你自己筆電上、掃得動自己專案的資安模型。
消耗大約 2GB 記憶體,不用付錢給誰。

全部在你自己的機器上驗證過。不買東西,不等任何人批准。

這三十天走的是一條線

不是三十個獨立的資安名詞。是跟著你用 AI 開發的整個過程走一遍,從你打開編輯器那一刻,到應用上線之後有人在打它。

第一段,你開發的時候(Day 1–8)

金鑰放哪、.env 到底安不安全、外洩了怎麼處置、你貼給 AI 的那段程式碼裡有什麼、它寫回來的東西誰檢查、生出來的程式碼在哪裡跑、它推薦你裝的套件存不存在、你裝的那些 MCP 拿到了什麼權限。

第二段,你交付出去之後(Day 9–19)

這時候使用者變成陌生人,信任邊界畫在哪、提示注入為什麼防不住、指令藏在模型讀的網頁裡怎麼辦、藏在工具描述裡呢、RAG 知識庫被投毒、防護 prompt 怎麼寫才驗得了、agent 該拿多少權限、AI 生的 CRUD 有沒有檢查授權、從模型講的話到真的執行動作中間該卡什麼、端點被狂打或被當免費 ChatGPT。

第三段,它跑起來之後(Day 20–22)

你的防線擋下了什麼你其實不知道、一次修補怎麼變成永久的測試、CI 什麼時候該擋住你自己。

第四段,你怎麼知道有效(Day 23–25)

打得到的地方在哪、全綠的測試是防住了還是根本沒打到、成績單不能由生它的人打。

第五段,你自己的工具(Day 26–30)

把一個筆電跑得動的資安模型架在自己機器上,用 CWE 來掃自己的專案,確認手上那個模型沒被掉包,最後誠實回答一句:你的專案現在防到什麼程度。

這條線上的每一站,都是 AI 進到開發流程之後才長出來的問題。有些看起來像老問題(金鑰、授權、注入),但成因換了,所以處理的順序和驗證的方式也跟著換。

今天從最短的那條開始吧 !

你昨天叫它「幫我串一下這個 API」

先想一個場景。

你開著 Claude Code 或 Codex,跟它說「幫我串一下這家的模型 API,做個介面可以聊天」。
三十秒之後畫面上出現一個能跑的東西,你按下去,模型回話了,你很爽。

那一刻你檢查了什麼?

大部分人檢查的是「會不會動」。會動就 commit 了。

但那三十秒裡它幫你決定了一件事:那個 API 要從哪裡打出去。
而在沒有其他指示的情況下,它給的答案幾乎都是同一個:從前端打,因為那是最短的路,程式碼最少,最快看到會動。

它沒有錯。你要的是「能跑的介面」,它給了你能跑的介面,成本、責任、金鑰放哪,這些你沒問,它就沒答。

這是這三十天要一直回來的那件事:AI 幫你做了很多決定,而它只回答你問的那個問題。

今天這一題就是它幫你做的第一個決定。

前端沒有祕密

你寫的前端程式碼,最後不是在你的電腦上執行的。

這句話講出來大家都同意。那就是網頁的運作方式:使用者連上你的網站,瀏覽器把你的 JavaScript 下載下來,在他的機器上跑起來。國中生都知道。

但這件事的含意,我認識的開發者裡沒幾個真的想過。

「我有混淆過啦」

上個月幫朋友看一個側邊專案。他做了一個會叫模型改寫文案的小工具,做得挺好,自己付 API 的錢,一個月幾百塊台幣,還在可以接受的範圍。

我問他金鑰放哪。

「前端啊,不過我有混淆過啦。」

我沒回答,請他按 F12,切到 Network,隨便送一次請求,點開那個打模型的請求,翻到 Headers。

Authorization: Bearer sk-proj-xxxxxxxxxxxxxxxxxxxx

完整的一串明文,就在那裡

他盯著看了三秒,說:「可是我把它拆成三段再拼回去欸。」

這句話我想了一下要怎麼回。後來我說:那個拼回去的動作,是誰的電腦做的?

拼回去的那台電腦,是他的

拆成三段的程式碼,也要下載到使用者的瀏覽器才能執行。要把三段拼起來的那一行,也是在他的機器上跑。拼完之後那個完整的金鑰,要寫進 request header,才發得出去。而那個 header,就在他的 Network 面板裡攤著。

你能寫進程式碼裡的每一個步驟,使用者都看得到。因為那些步驟本來就是在他家執行的。

https://ithelp.ithome.com.tw/upload/images/20260801/201389249wvz1NiNXR.png

這張圖之後會一直回來。今天只有兩個框,紅色那格就是今天的問題:金鑰待在右邊,後面幾天我們會在同一張圖上加東西,看它怎麼長成整個系列的信任邊界。

所以不管你把它拆幾段、用什麼函式包幾層、換成什麼奇怪的編碼,最後一定有一個瞬間,完整的金鑰會出現在他的機器上。不然請求發不出去。

反過來說也成立,而且這句話比較好記:瀏覽器帶著 bearer API key 發得出那個請求,就代表那把金鑰到過瀏覽器。

這是這一整天唯一需要記住的句子。三年後你換一套框架、換一家供應商、換一種語言,它還是成立,因為它講的不是某個工具的行為,是誰的機器在跑那段程式碼。

順帶把最容易誤會的那個講掉:VITE_NEXT_PUBLIC_ 開頭的環境變數,build 完就躺在 bundle 裡,跟寫死在原始碼沒有分別。它叫環境變數沒錯,但它的環境是使用者的瀏覽器。

先確認你有沒有中招

兩招,五分鐘。

第一招剛才示範過了:DevTools → Network → Fetch/XHR,點那個打模型的請求,看 Headers。

第二招是直接翻 build 出來的東西:

npm run build
grep -rIl "sk-" dist/

sk- 換成你家供應商的金鑰前綴。這個檢查只列出檔名,不會把完整金鑰印進終端紀錄。找得到代表確定洩漏;找不到不代表安全,因為其他前綴、編碼或未走到的請求仍可能漏過。

這兩招等一下還要用,因為改完之後要拿同一組指令回來驗。

手上沒有適合的專案也沒關係,用這個系列的範例專案,它內建了一模一樣的洞。

金鑰該待的地方

既然瀏覽器一定會看到它送出去的東西,那答案就只剩一個:別讓瀏覽器碰到金鑰,中間放一台你控制的機器,讓它去跟供應商講話。

import express from "express";

const app = express();
app.use(express.json({ limit: "16kb" }));

app.post("/api/chat", async (req, res) => {
  const message = req.body?.message;
  if (typeof message !== "string" || message.length === 0 || message.length > 4000) {
    return res.status(400).json({ error: "invalid message" });
  }

  try {
    const r = await fetch("https://api.provider.example/v1/chat", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.PROVIDER_API_KEY}`,
      },
      signal: AbortSignal.timeout(30_000),
      body: JSON.stringify({
        model: process.env.PROVIDER_MODEL,
        messages: [{ role: "user", content: message }],
      }),
    });

    return res.status(r.status).json(await r.json());
  } catch {
    return res.status(502).json({ error: "provider unavailable" });
  }
});

app.listen(8787);

前端那邊,把原本打供應商的網址改成 /api/chat,只送 { message },金鑰整段刪掉。

先停一下:這段只解決「供應商金鑰不進瀏覽器」,還不能直接上線。/api/chat 目前沒有身分驗證、每人額度與速率限制,公開部署後仍可能被當成免費模型端點。之後會把這些補齊;今天先把信任邊界畫對。

https://ithelp.ithome.com.tw/upload/images/20260801/20138924kl0jlX6xwy.png

跟上面那張對照著看,差別只有一個:金鑰那格從右邊移到左邊,邊界沒有消失,是你把該留在裡面的東西留在裡面了。

這段骨架可以叫 AI 幫你起稿,但輸入限制、逾時與錯誤路徑仍要自己確認,接下來那一步更不能只問它。

它看不到你的 Network 面板。你問它「這樣安全了嗎」,它只能根據你貼給它的片段回答
「應該沒問題了」。那不是驗證,那是複述。讓 AI 宣告安全,等於沒驗。

回去看一眼

重跑剛才那兩招。瀏覽器不該再出現供應商 API key,也不該直接向供應商端點送出請求;應用自己的登入權杖仍可能放在 Authorization,不能只看這個 header 名稱。build 產物裡,grep 也不該再列出含有供應商金鑰前綴的檔案。

我自己第一次做這件事的時候卡在這裡。前端明明改乾淨了,grep 還是一直找得到。
找了快半小時,才發現我在搜舊的 build 產物。dist/ 沒清,我從頭到尾都在看上一版的檔案。

rm -rf dist 再 build 一次就沒了。

這件事講出來有點蠢,但它後面還會再發生好幾次:驗防護 prompt 的時候、驗回歸測試的時候。
驗收的第一個動作,永遠是確認你在看的是新的東西。

那使用者自己填金鑰呢

有人會問:如果金鑰是使用者自己填的,那前端放著總可以了吧?他的錢他自己負責。

這種模式叫 BYOK,使用者自帶金鑰,在 AI 產品裡很常見。

換成使用者自己的金鑰,風險沒有消失。長效 bearer API key 一旦進入網頁,就會暴露給該頁面執行的程式、第三方腳本、XSS 與惡意擴充套件。

差別只在燒的是誰的錢,還有出事時誰要負責說明。通常還是你。

BYOK 底下還有一整套託管、權限、儲存與撤銷問題,那不是今天的範圍。
如果供應商提供 OAuth 或短效權杖,應優先使用。今天只要記住:
具備付款或後端權限的長效 bearer API key,不該內嵌或持久化在前端。
公開鍵或可發布鍵不是同一類憑證。

今天結束時你手上有什麼

一個後端代理前端不再拿著任何供應商金鑰。還有一份自己跑過的驗收紀錄,兩招都做了,不是憑印象覺得改好了。

如果你今天打開 Network,發現真實金鑰已經在外面躺了三個月,先別急著刪 repo,也不要只刪程式碼。
先到供應商後台撤銷或輪替那把金鑰,讓舊金鑰失效。後面我們會再完整處理使用紀錄、影響範圍與 repository 歷史;順序很重要,做錯會白忙一整天。

明天要問下一個問題:金鑰搬到伺服器的環境變數了,那裡就安全嗎?

.env 不是魔法盾牌。

明天見。


系列文
你該防的不是駭客,是你自己的 AI:在本機驗證你的防線1
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言