跳到主要內容

    讓 AI 代理自動批准指令有多危險:Pixel Crew 的 Bash 圍欄是怎麼補的

    Pixel Crew 同時跑最多 20 個 Claude Code 與 Codex,自動批准不開沒辦法用。10 月 7 日那次安全審視抓到八種繞過,每一種附一行會繞過的指令、怎麼修,還有明講修不掉的部分。

    重點

    • 自動批准要靠「窄白名單」放行,黑名單只負責把拒絕理由講清楚;反過來做一定被繞。
    • 「唯讀」指令配上某些旗標就能寫檔或執行程式,白名單要比對旗標,不能只看指令名。
    • 檢查要看完整指令,不能看卡片上截斷的文字;超過 20,000 字元的指令一律交給人。
    • Windows 的 rm -rf 對等指令、跨磁碟與 UNC 路徑、symlink 的下載目錄,都是獨立的洞。
    • 寫入重導向的圍欄是防禦縱深不是圍牆,認不出的形狀就不擋,殘餘風險要明講。
    目錄

    Pixel Crew 是我做的本機工具,把多個 Claude Code 與 Codex session 放進一間像素辦公室,最多同時 20 個。這種用法下自動批准一定要開,不然 20 個代理每做一件事都要你按一次允許,等於沒有自動化。但自動批准的另一面是,代理一旦被餵了惡意內容,或者單純判斷錯誤,它跑的指令不會再經過你。10 月 7 日我對這塊做了一次完整審視,抓到八種繞過,當天全部修掉。這篇一個一個講:怎麼繞、為什麼會漏、怎麼補。程式碼都在公開 repo,連結放在各段。

    像素辦公室裡一個 NPC 停在「允許執行這個指令?」的卡片前,等人按拒絕、本次皆允許或允許

    沒開自動批准的樣子:NPC 停在原地等你按。20 個 NPC 都這樣,你一整天就在按允許。

    先講結論:白名單放行,黑名單只負責說理由

    第一版的想法很直覺,列一張危險指令清單,rm -rf、sudo、mkfs、curl | bash,沒命中就放行。審視之後整個反過來:自動批准只靠一張很窄的白名單,只放行認得出來的唯讀指令;危險指令清單還在,但它的工作變成「被拒絕的時候把理由講清楚」。

    原因很簡單。黑名單只能擋你想得到的形狀,shell 能表達同一件事的方法太多了。下面八個洞,有六個都是「這條指令長得不像危險指令,但做的事是」。

    Pixel Crew 現在有四檔:

    模式 行為
    off 每件事都問
    safe 只放行白名單裡的唯讀指令,其餘都問
    full 預設放行,只擋黑名單命中的毀滅性指令
    invincible 什麼都不擋,等於 Claude Code 的 --dangerously-skip-permissions

    full 有一件事要講明:非 Bash 的工具呼叫,包括每一台接上來的 MCP server 的每一個工具,在 full 模式是無條件放行的。沒有逐工具的風險分級。所以 full 的意思不是「對 shell 寬鬆一點」,是「MCP server 能做的事都不再問」。這句話我寫在程式碼註解裡,也要求所有介面文案照這個意思寫。

    safe 模式一條 Bash 指令要連過五關才會自動放行,任何一關沒過就回到問你。後面八個洞裡有五個,都是其中某一關原本漏了:

    指令名在唯讀白名單 旗標不在寫入/執行表 引號外沒有( ) { } 與萬用字元 串接的每一段都唯讀 重導向只有丟棄輸出 自動放行 交給人按 超過 20,000 字元的指令不進這條流程,直接交給人。
    safe 模式的五關。虛線是任一關沒過的去向。

    洞一:唯讀指令配上旗標就會寫檔或執行程式

    白名單一開始認的是指令前綴:git diff、git status、rg、ls、cat 這些。問題是同一個指令配上某些旗標,就不唯讀了。實測過的兩個:

    git diff --output=/tmp/outside.txt   # 會把外部檔案截斷成空檔
    rg --pre ./evil.sh TODO              # 會對每個檔案執行 evil.sh

    修法是在前綴比對之後再比對一張「寫入或執行旗標」的表,而且比對前先把引號跟反斜線剝掉,因為在 shell 裡 "--output=x"、--out""put=x、\--output 都等於 --output=x。tsc 的選項名稱不分大小寫,--OUTDIR 一樣有效,所以那條要忽略大小寫。npm、pnpm、yarn 的 --script-shell、--node-options、--prefix 也在表上。

    sed 乾脆不用前綴放行。就算是 sed -n,也能用 w 指令寫檔、用 e 指令執行外部命令,例如 echo x | sed -n "w /path"。白名單只收 Codex 讀檔時慣用的那一種整句形式,sed -n '1,200p' 檔案,腳本只能是行號範圍加 p,檔案參數不能是選項、不能有萬用字元。

    洞二:zsh 不需要分號就能執行另一個指令

    白名單原本用一組 shell 中繼字元判斷「這條指令有沒有夾帶別的東西」:換行、;、&、|、<、>、反引號、$(、${。這在 bash 大致夠用,但 macOS 的預設 shell 是 zsh,Claude 的 Bash 工具也跑在上面,zsh 多了幾種不需要任何中繼字元的寫法:

    ls *(e:'touch pwned':)    # glob qualifier,列檔案的同時執行 touch
    cat =(cmd)                # process substitution
    git diff --out{put,}=x    # brace expansion 拼出上面擋掉的旗標

    修法:引號外出現 (、)、{、} 任何一個,就不算白名單安全指令。

    洞三:萬用字元會展開成檔名,檔名可以長得像旗標

    rg TODO *.ts 看起來無害。但 *.ts 會在執行前被 shell 展開成目錄裡的檔名,而檔名可以就叫 --pre=x.ts。洞一那張旗標表是在展開前比對的,看不到展開後才出現的選項。

    修法只對有危險旗標的工具收緊:git、rg、eslint、tsc、npm、pnpm、yarn 這幾個,引號外出現 *、?、[ 就不自動放行。ls *.ts、cat *.md 這類沒有能寫檔或執行的旗標,照舊可以用萬用字元,不然白名單會窄到沒人想用。

    洞四:寫入重導向有很多種寫法

    Pixel Crew 對檔案工具有一道圍欄:寫入目標逃出 workspace 就硬擋。Bash 沒有結構化的路徑參數,所以另外做了一個函式,掃出指令裡「寫入型重導向」的字面目標,逃出 workspace 就回 true。第一版只認 > 跟 >>。審視時補上的:

    cmd 1> /etc/x        # 帶 fd 編號
    cmd 2>> ~/.zshrc     # fd 加 append,目標還要先展開 ~
    cmd &> $HOME/x       # stdout 跟 stderr 一起寫,$HOME 在雙引號內會展開、單引號內不會
    cmd >| /tmp/x        # 強制覆寫,zsh 也認 >!
    cmd >& /tmp/x        # >& 後面接檔名就是寫檔,不是 fd 編號

    同時要排除不是寫檔的:2>&1、>&2、>&- 是 fd 複製或關閉,/dev/null、/dev/stdout 是丟棄輸出。~user、~+ 這類靜態判定不了的,一律當成逃逸。

    這個函式的註解我寫得很白:刻意不做完整 shell 解析,這是防禦縱深,不是圍牆。它只認字面重導向目標,對變數展開(p=/etc; echo > $p)、子殼、python -c "open(...)"、cp、mv、tee 的目的地,一律「認不出等於不擋」。測試檔裡有一個案例叫 documented residual bypasses are NOT flagged,專門確認這些繞過真的不會被擋,免得哪天有人以為圍欄比實際上厚。

    擋得到:字面上的寫入重導向 cmd > /etc/x cmd 2>> ~/.zshrc cmd &> $HOME/x cmd >| /tmp/x cmd >& /tmp/x 目標逃出 workspace 就擋;2>&1、/dev/null 不算 擋不到:認不出就不擋,寫在文件裡 p=/etc; echo x > $p python -c "open('/etc/x','w')" echo x | tee /etc/x cp x /etc/x ( echo x > /etc/x ) 防禦縱深,不是圍牆;full 模式下這些會過
    寫入重導向圍欄的兩邊。右邊是刻意不處理、但明講的部分。

    洞五:檢查看的是卡片文字,不是完整指令

    核准卡片顯示的指令有長度上限,20,000 字元。原本的檢查是對卡片上的文字跑,所以在危險片段前面塞兩萬個字的填充,檢查就只看得到無害的開頭。

    修法兩條:危險指令的比對一律對完整指令跑;長度超過上限的指令不自動放行,直接交給人,理由寫「指令超過 20,000 字元,無法完整顯示與檢查」。卡片顯示不完整的東西,本來就不該有人幫你按允許。

    洞六:Windows 的防線根本沒接上

    黑名單有 Unix 的 rm、mkfs、dd,卻漏了 Windows 的對等指令,而 Windows 是 Pixel Crew 的主要平台之一。補上的:

    Remove-Item -Recurse -Force C:\repo\dist
    Remove-Item build -r -fo        # PowerShell 參數可以縮寫
    rd /s /q C:\repo\dist
    del /f /s /q *.log
    format D: /q

    幾個細節。del、erase、rd、rmdir、ri 都是 Remove-Item 的別名,要一起認。format 後面要直接接磁碟代號才算,不然會誤中到處都是的 --format 旗標。Windows 的指令與別名解析不分大小寫,Git Push --Force 跟 git push --force 一樣,所以整組比對都要忽略大小寫。

    路徑也有 Windows 專屬的洞。判斷「目標有沒有逃出 workspace」用的是 path.relative(),看結果是不是 .. 開頭。但跨磁碟(D:\)、UNC(\\server\share)、\\?\ 路徑跟 workspace 沒有共同的根,relative() 會直接回傳目標的絕對路徑,不是 .. 開頭,第一版就放過去了。修法多一個條件:結果是絕對路徑也算逃逸。

    每個 NPC 有一個 outbox 目錄,裡面的檔案可以從網頁下載。路徑檢查會擋 ..,但如果 outbox 本身是個 symlink 指到別處,檢查過了、讀到的卻是別的地方的檔案。修法是提供檔案前先確認 outbox 目錄不經過 symlink,workspace 裡的路徑檢查本來就會逐層 lstat,這裡補成一致。

    洞八:Read 跟 WebFetch 不該預先放行

    為了少跳提示,第一版把 Read、Glob、Grep、WebSearch、WebFetch 當成唯讀工具預先放行。問題是 Read 可以讀 workspace 外的檔案,WebFetch 可以抓任意網址,前者外洩,後者是把外部內容餵給代理的入口。現在這兩個在 safe 模式一樣要經過批准,只有讀 workspace 內的檔案才直接過。

    還有一個不是洞的坑:Codex 的指令外面包了一層殼

    Codex CLI 回報的每一條指令都包成 /bin/zsh -lc <cmd>,內層有單引號時改用雙引號、單字時不加引號。前綴白名單永遠對不上這個外殼,等於 Codex 在 safe 模式什麼都要問。修法是只拆「一層」,而且只拆引號內不可能有跳脫或展開的形式;拆不乾淨就回 null,照一般判定走,不會因此多放行任何東西。

    修完之後的規則

    • 自動放行只看窄白名單;黑名單負責拒絕理由。
    • 白名單比對指令名、也比對旗標;比對前剝引號與反斜線。
    • 引號外出現 (){} 或對旗標敏感的工具遇到萬用字元,不放行。
    • 串接指令逐段檢查,每一段都唯讀才整條放行;只容忍 2>&1、2>/dev/null 這類丟棄輸出。
    • 寫入重導向逃出 workspace 就擋,認不出的形狀不擋,但寫在文件裡。
    • 看完整指令,超過 20,000 字元交給人。
    • Windows 的刪除與格式化指令、跨磁碟與 UNC 路徑,跟 Unix 同等待遇。
    • 只有使用者親手切換才能進 full 與 invincible,介面要說清楚 full 對 MCP 工具是全放行。

    殘餘的風險我也列在文件裡:變數展開、子殼、直譯器內的檔案操作、tee 與 cp 的目的地,這些在 full 模式下會過。開 full 的人要知道自己換到的是什麼。

    改到的檔案:dangerousCommand.ts、bashWriteFence.ts、safeLocalPath.ts,以及對應的測試。發現繞過請走 私密弱點回報,七天內回覆。

    常見問題

    Pixel Crew 的 full 模式跟 Claude Code 的 --dangerously-skip-permissions 一樣嗎?

    不一樣。full 仍然用黑名單擋 rm -rf、sudo、mkfs、curl 接 bash 這類已知的毀滅性指令,只是其他的不再問。真正等同 --dangerously-skip-permissions 的是第四檔 invincible,那一檔連黑名單都不擋,介面上寫明風險自負。

    為什麼不乾脆做完整的 shell 解析?

    因為那是無底洞,而且做完還是不可靠。變數展開、子殼、python -c 裡的 open()、tee 的目的地,每一種都要處理,處理完還有下一種。我選擇只擋認得出來的形狀,把認不出來的寫成文件,讓開 full 模式的人知道自己承擔什麼。

    一般人自己用 Claude Code,自動批准要怎麼設?

    先用最窄的那檔,看一個星期被攔下來的都是什麼,再決定要不要放寬。接了 MCP server 更要保守,因為 full 模式對 MCP 工具是無條件放行,沒有逐工具的風險分級。

    分享LINEThreadsX