AI 程式碼安全審查

AI 寫得出程式,不等於它交付的是安全程式

作者請 Claude Code 寫擋機密資訊的 hook,程式用了沒加單引號的 heredoc 加字串插值,檔案內容會被當成 Python 程式執行,是教科書級的 code injection;五個 hook 共十個注入點,而抓到漏洞的是另一個 AI subagent。結論是三條紀律:敏感操作永遠懷疑選項的正確性、AI 寫的 code 用另一個 AI 審、commit 不 push 讓最後防線留給人。
AI 寫得出程式,不等於它交付的是安全程式:文章重點卡

AI 寫得出程式,不等於它交付的是安全程式

你將學到什麼

漏洞長什麼樣

heredoc 沒加單引號會先做變數展開,檔案內容直接變成可執行程式。

正確的寫法

heredoc 加單引號、變數改走環境變數傳遞,資料與程式碼絕不混在一起。

AI 審 AI

寫 code 與挑毛病要用不同 context,同一個 session 自我審查會有偏誤。

最後防線

commit 是本地可 reset,push 是公開爬不回來,push 只留給人類。

事情是這樣的,昨天晚上用 Claude Code 開了一個新專案的骨架 20 幾個檔案、7 個架構決策、5 個 hook、2 個 subagent、5 個 skill。全部都是 AI 寫的。然後我差點把一個 Remote Code Execution(RCE)漏洞給 commit 上去。

但!抓到這個漏洞的不是我,是另一個 AI。幸好我有「用 AI 審 AI」的習慣,不然真的會 G 掉!

所以這篇文章不是要跟你說「AI 寫 code 多厲害」,而是要很誠實的告訴你:就算是很強的 AI 所寫的 code 也會有漏洞,而且會是你一時之間看不出來的漏洞。

事情是這樣發生的

我請 Claude Code 幫我寫 5 個 hook(.claude/hooks/ 裡面那種),其中一個叫 block-secrets.sh,職責很單純:在 Claude 要寫入任何檔案之前,檢查檔案內容裡有沒有 API key、密碼、.env 這種敏感資訊。有的話就擋掉。

看起來很安全對吧?畢竟 Hook 本身就是拿來做 security 的。

Claude 很快寫完了,我瞄了一眼,覺得邏輯很合理,用 bash 收 stdin、丟 Python 用 regex 找 secret pattern、找到就 exit 2 擋掉。

程式碼就像圖片中那樣,如果你看不出問題,那就跟我昨天晚上一樣。

AI 審 AI,抓出 injection

我做完第一輪 review 的時候其實沒發現問題,畢竟 Claude Code 用起來太流暢,稍微一鬆懈就可能一路 Yes 下去,但我還是依照我給自己定的規定:務必讓另一個 AI subagent 再 review 一次。

這個 subagent 吐出的話一開始我還沒警覺到,直到我讀完了整段再重新看一次的時候才嚇掉一身的貓毛:

「這個 heredoc 用的是 <<PYEOF 而不是 <<'PYEOF'(沒有單引號),會讓 bash 先做變數展開。然後 """$FILE_CONTENT""" 直接把檔案內容插到 Python 字串裡 —— 如果檔案內容是 """; import os; os.system('rm -rf ~'); """,Python 會把它當成真的程式碼執行。」

翻譯成人話就是:這個 hook 原本是設計來組擋惡意內容的,結果它自己就是惡意內容執行的入口!!!

Claude 寫一個檔案,內容有特定字串,hook 為了檢查這個字串,反而把字串當成 Python 程式執行了。

這是教科書級別的 code injection!!!

更恐怖的是 Claude Code 寫出來的一共有 5 個 hook 都用了同樣的模式,其中 audit-log.sh 裡就有 2 個 heredoc 都中招了。

全部掃完加起來一共 10 個注入點,全部都是 AI 寫的,全部都差點 push 到 GitHub 😔

正確的寫法長這樣

修法其實很簡單,但你要知道兩件事:

① heredoc 要加單引號

`<<'PYEOF'` 跟 `<<PYEOF` 差一個引號,行為完全不一樣!

沒引號 → bash 會先展開 `$變數`,然後才丟給 Python。

有引號 → bash 完全不動這坨文字,原封不動交給 Python。

② 變數要用環境變數傳,不要字串來插值

差別在哪?

字串插值 = 把使用者的輸入變成程式碼的一部分。

環境變數 = 把使用者的輸入變成資料,程式用 API 去讀。

這跟 SQL injection、XSS、command injection 一樣的道理:絕對不要讓使用者資料跟程式碼混在一起。

我以為這種基本觀念 AI 會懂,結果 Claude Code 寫出來得就是字串插值的版本。

Vibe Coding 的三條保命須知

這次事件讓我把過去 Vibe Coding 的心得濃縮成三條,分享給你。

須知①:永遠懷疑選項的正確性

Claude Code 每次要執行敏感操作,像是跑 skill、寫檔案、執行指令時,都會彈確認視窗:

Do you want to proceed?
❯ 1. Yes
2. Yes, and don't ask again for this project
3. No

我自己是很容易手滑選 2,因為我不想每次都被問,然後就會後悔。

我給自己的防呆規矩是:
第一次用的 skill、hook、agent,永遠選 1。
用過三次確認它行為可預期,才升級成 2。

這多出來的 3 秒鐘,就是你能攔下 AI 亂搞的時刻。

須知②:讓 AI 審 AI

一個 AI 寫的 code,用另一個 AI review。

我今天用的方法很土砲!

Claude Code 寫完 hook,開另一個 subagent(@schema-reviewer 模式)把 hook 丟給它,問「找出所有的 security 風險」。

然後它抓到了 RCE,我沒抓到。

我自己當然有錯,因為我應該一行一行看仔細的,但對 AI 來說「寫 code 的腦袋」跟「挑毛病的腦袋」用的是不同的 context、不同的 prompt。

同一個 AI 在同一個 session 裡自己 review 自己,它會有 bias 的現象,覺得「我剛寫的應該沒問題」。

換一個 session,或換一個 agent ,或換一個角度,它才抓得到。

就跟自己在寫書一樣,你寫的書自己一校二校三校都找不到錯字,別人卻一秒就能看到😂

須知③:commit 但不 push

我在 CLAUDE.md 裡寫了一條硬規則:Claude can `git add` and `git commit`, but never `git push`. Push is a human-only action.

為什麼?

因為 commit 是本地的,可以 reset;push 是公開的,爬不回來。

今天如果我沒這條規則,那些有 RCE 漏洞的 hook 會直接上 GitHub。萬一 repo 是 public 的,任何人 clone 下來跑 Claude Code,都可能被我的 hook 攻擊。

「push 是人類才可以進行的操作」這幾個字,今天救了我。

給還在觀望 Vibe Coding 的你

我知道這篇讀起來有點嚇人,AI 寫的 code 有漏洞、差點上 production。

但我寫出來不是要勸退你,恰恰相反,因爲我一個上午做完了過去需要一週的工作量。7 個 ADR、20 幾個檔案、完整的 project scaffold。

但「AI 幫你做事」不等於「AI 幫你做對事」。

這兩者之間差的就是這三條紀律:

① 永遠懷疑選項的正確性,不要手滑選「永遠允許」
② AI 審 AI,寫 code 的跟 review 的要分開
③ commit 不 push,最後一道防線永遠是人

我會繼續用 Claude Code 做這個專案,但我不會把它當「自動駕駛」,它只能是副駕駛,而方向盤必須牢牢在我手上。

額外收穫

昨晚的漏洞,後來變成我最好的 Demo 素材之一。中午跟客戶 demo 這個專案的時候,原本想秀架構、秀 ADR、秀那些華麗的檔案結構。

後來我改了策略,直接把昨晚的這個意外講給客戶聽。

客戶的反應超預期!

他說:我找過其他 AI 開發的同業,他們都說『AI 很強、什麼都能做』。妳是第一個告訴我『AI 會出錯,而我有自我規範去抓錯』的人。

揭露了 AI 的缺陷,反而建立了更高的信任。

這就是 Vibe Coding 的第四條隱藏須知:別賣「AI 多厲害」,要賣「我怎麼管 AI」。

前者賣的是商品,後者賣的是專業。

#但我還是很悔為什麼第一時間我自己沒有發現問題呢?
#有問題的是圖片中的第三行指令

本文原發表於 2026/04/23 的 Facebook 貼文,原文照登,僅調整網頁排版;文中提及的產品、價格與活動以當時為準。

繼續追蹤酒Ann想看更多 AI 系統、工具實測、品牌方法與生活觀察,歡迎前往 酒Ann 的 Facebook

延伸學習

把這篇文章分享給需要的人FacebookLINEThreadsX

常見問答

為什麼作者自己第一輪 review 沒看出漏洞?
因為邏輯看起來很合理:bash 收 stdin、丟 Python 用 regex 找機密樣式,找到就擋掉。問題藏在 heredoc 少了單引號與字串插值這種一時看不出來的細節,最後是另一個 AI subagent 抓出來的。
heredoc 加不加單引號差在哪?
沒加引號時 bash 會先做變數展開再丟給 Python,使用者輸入就混進程式碼;加了引號 bash 完全不動這段文字,原封不動交給 Python,輸入只是資料。這跟 SQL injection、XSS 同一個道理:不要讓使用者資料跟程式碼混在一起。
為什麼要規定 AI 只能 commit 不能 push?
因為 commit 是本地的、可以 reset;push 是公開的、爬不回來。這條寫在 CLAUDE.md 的硬規則攔下了帶著 RCE 漏洞的 hook;如果 repo 是公開的,任何人 clone 下來跑都可能被攻擊。
這次意外對客戶簡報有什麼影響?
作者把事件直接講給客戶聽,客戶反而更信任:別人都說 AI 什麼都能做,她是第一個說 AI 會出錯而我有自我規範去抓錯的人。揭露缺陷反而建立了專業信任,別賣 AI 多厲害,要賣你怎麼管 AI。