iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
AI Security

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

Day 7|AI 幻想出來的套件名,有可能變成真的

  • 分享至 

  • xImage
  •  

昨天我們把來路不明的腳本關進容器裡跑,重點是「執行的時候給它多少」。今天走到前面一格:那個東西是怎麼進到你機器上的。

多半是這樣。你問 AI 怎麼做某件事,它給你一段程式碼,前面附一行 npm install。你複製、貼上、回車。

昨天結尾丟了三個問題:那個套件是誰發的、上一次更新是什麼時候、還有它存在嗎。今天從最後那個開始問,另外兩個都掛在它身上:不存在的東西沒有發布者,也沒有更新時間。

先讓它答對幾題

我用手邊一個小模型問了五個常見題目(Antigravity CLI 的 Gemini 3.5 Flash,Low 檔,2026-08-07):Node.js 驗 JWT、API 速率限制、輸入驗證、結構化日誌、讀環境變數。

回來的是 jsonwebtokenexpress-rate-limitzodwinstondotenv

五個都在,而且都是那個領域你會期待看到的答案,換成我自己來選也差不多。

到這裡為止,「AI 推薦的套件不能信」聽起來像在沒事找事。

換八題比較少人問的

我把題目換成冷門一點的。不是刁鑽,是那種你真的會在專案裡遇到、但 Stack Overflow 上沒幾篇的:

回傳 名字 題目
200 @opentelemetry/instrumentation-express Express request 轉 OTel span
200 taiwan-id-validator 身分證字號驗證
200 prisma-json-schema-generator Prisma schema 轉 JSON Schema
200 bullmq Redis stream exactly-once 消費
200 nginx-log-parser nginx log 解析
200 @anatine/zod-openapi Zod 轉 OpenAPI
404 代號 D Fastify mTLS 客戶端憑證
404 代號 E ConfigMap 熱重載

八題裡兩個 404。那是我拿名字去打 npm 官方註冊處的唯讀 API 得到的回應,意思是註冊處上沒有這個名字。

那兩個名字我換成代號 D 跟 E,理由跟下一節那三個一樣。可以驗的部分照留:兩個都沒有 scope,2026-08-07 查註冊處都回 HTTP 404。

回 200 的那六個照留,寫出來不會給誰多一個可以搶的位置。

我第一個反應跟你可能一樣:那不就結案了嗎,裝的時候會失敗啊。

我以為隨機的東西沒人搶得到

我把那兩題單獨拉出來,同一句提示詞跑五次,看它每次編出來的名字一不一樣。

(沒有 scope 的那三個名字,我用代號取代)
run 1  @fastify/client-cert           chokidar
run 2  A                              C
run 3  A                              chokidar
run 4  @fastify/client-authorization  B
run 5  @fastify/client-certificates   C

可以驗的部分在這裡:

代號 有無 scope 五輪出現 2026-08-07 查註冊處
A 2 次 HTTP 404
B 1 次 HTTP 404
C 2 次 HTTP 404

這三格原本寫的是真名字,遮掉是我改過的主意。推翻我的是下一節那篇論文:幻覺名多半不是把真名字打錯一兩個字元(most package hallucinations are not simple off-by-one errors),坐在家裡猜不出來,那寫出來就是把原本拿不到的東西遞出去。那篇論文也一樣,程式碼與資料集都公開,唯獨名單不公開:we will not release our master list of hallucinated package names

七個不重複的名字,六個在註冊處上不存在,只有 chokidar 是真的。

那六個要分兩堆看。@fastify/ 開頭那三個是 scoped 名字,發布權綁在 scope 擁有者身上(npm 官方文件,2026-08 查證:When you sign up for an npm user account or create an organization, you are granted a scope that matches your user or organization name.docs.npmjs.com/about-scopes),@fastify 已經有人在用,回 404 只代表 fastify 沒發過這個名字,不代表它擺在架上等人拿。A、B、C 沒有 scope。回 404 也不保證註冊得成(名字可能不合發布規則、可能被 npm 保留),但只要合規又沒被保留,那個位置就是先到先拿。所以先看名字有沒有 @:沒有,那個位置是空的;有,要問那個 scope 有沒有人在用。

不過,六個不是重點:

五輪兩題的名字矩陣,兩排各五格,內容就是上面那段輸出。紅框標的是兩輪問出同一個名字、註冊處上卻查不到的那四格:上排兩格代號 A、下排兩格代號 C,那是攻擊者有標的可搶的位置。灰底虛線是下排那兩格 chokidar,一樣重複,但它真的存在

紅框那四格是重點:A 兩次,C 兩次。

只出現一次的名字一樣註冊得到,只是對想守株待兔的人沒什麼吸引力。你今天問出一個怪名字、明天問出另一個,攻擊者沒有穩定的標的可以打。會穩定重複出現的名字才有標的。 有人只要先去註冊那個名字、放一包東西進去,之後再用同一個模型問到同一題、又收到同一個名字的人,照著跑 npm install 都會裝成功,而且不會有任何錯誤訊息。不是每個人都會收到,我那兩個名字五輪各兩次。

所以危不危險,幻覺率跟重複率要分開問:幻覺率回答它多常發生,重複率回答攻擊者有沒有一個穩定的標的。

而重複率剛好是你最不容易看到的那個數字,因為你只會問一次。你看到的是一個名字,不是一個分布。

我這是玩具規模,有人量過真的,也有人真的把那個位置補上去

八題五輪是我一個早上跑出來的,樣本小到不能主張任何比例。

真的做過的是 USENIX Security 2025 那篇《We Have a Package for You!》(Spracklen 等人,arxiv.org/abs/2406.10279,數字出自全文 v3)。他們拿 16 個模型生成 576,000 份程式碼再回頭追問,蒐集到 205,474 unique examples of hallucinated package names,PyPI 跟 npm 合計。幻覺套件的平均比例,商業模型至少 5.2%,開源模型至少 21.7%。

二十萬個不重複的假名字,聽起來像一份可以對照的黑名單,但它不是。論文量到 81% of hallucinated packages are generated by only one model,而那 16 個模型是 2024 年那批,沒有我今天用的這個。沒有一份跨模型又持續有效的黑名單,一個名字現在存不存在還是要當場查。 這也是「同一個模型」那句的來源:換一家問,標的多半會跟著換。

重複那件事論文量過:抽 500 個會產生幻覺的提問,同一題各再問十次,43% of hallucinated packages were repeated in all 10 queries, while 39% did not repeat at all across the 10 queries。這個分布在兩端各有一個高峰,十次裡重複超過一次的佔 58%。

我那五輪不能拿來跟這幾個比例對,輪數與抽樣方式都不同,我也沒有一個名字五輪全中,同的只有方向。一次都沒重現的那批比較不會是搶註標的,但十次沒重現推不出它以後不會再出現。

「有人去搶註」到這裡都還是我推的。它發生過一次,整條鏈都有紀錄(以下 2026-08 查證)。

eslint-plugin-unused-imports 是一個 ESLint 外掛,上週近九百萬次下載,而 unused-imports 這個短名字原本不存在。這批套件 2025 年 8 月開始上架,10 月被資安公司 Koi 揭露,npm 下架;unused-imports 在那批名字裡,來源沒寫它自己哪天註冊。名字是從幻覺來的,這是 Koi 的判讀,不是模型輸出或攻擊者自白:They're carefully chosen to exploit a new vulnerability: LLM hallucinations. 同一批有 126 個,Koi 把這批叫 PhantomRaven。

它怎麼跑起來的?你在 npm 上看到的那包是乾淨的,preinstall 不在它身上。它的相依欄位填的不是版本號,是一個 HTTP 網址,npm install 才照那個網址去攻擊者的主機抓真正那包回來,preinstall 在那一層。抓下來就跑,還沒裝完它已經用你的身分把 npm token、GitHub 憑證與 CI/CD 機密拿走,就是 Day 6 在 ~/.npmrc 裡指給你看的那個 _authToken,加上 Day 1 到 Day 3 那條金鑰線。

代價寫在 GHSA-gcxg-769g-4p5h 描述的第一句:Any computer that has this package installed or running should be considered fully compromised. 後面兩句是:那台機器上的祕密與金鑰都要從另一台機器換掉,就算移除套件也不保證清得乾淨。

200 也不代表可以裝

八題裡有六個回 200。存在是存在,然後呢?

我寫了一支腳本,把上面幾個名字、一個一定在的對照(express)、再加一個我自己編的名字餵進去,列出註冊處上看得到的東西:

判定     週下載  最後發布  版本 維護者 原始碼  套件
查到  126730328   2025-12   288      5     有  express
查到          7   2016-04     4      1     無  nginx-log-parser
沒有          -         -     -      -      -  npm-registry-probe-0807-zzq
查到       1842   2015-11    16      1     有  express-rate-limiter
查到     740835   2025-04    47      1     有  @anatine/zod-openapi

第三行 npm-registry-probe-0807-zzq 是我自己編的,讓你看到「沒有」那一列長什麼樣:每一欄都是 -。真的問出來的那兩個 404 沒放進來,理由跟代號一樣。nginx-log-parser 是另一種形狀:2016 年之後沒再發過版,連上游儲存庫都沒填。

最值得記的是 express-rate-limiter 那行。它比開頭那個 express-rate-limit 多一個 er,最後發版是 2015 年 11 月,2026-08-07 查,上週 1842 次下載,這個數字每天都在動。真正的那個我另外查了一次:2026-08-04 剛發過版,129 個版本,兩個維護者,上週五千九百多萬次。

它不是惡意套件,就是一個沒人維護的舊東西。要看的是那個 1842:十年沒發過版還在被裝,那些下載是誰,註冊處只回一個整數,打錯字、舊 lockfile 每天在 CI 裡裝一次、鏡像站在抓,我分不開。unused-imports 也一樣,下架後留了 0.0.1-security 佔位符,上週還有 329 次下載。

下載數往上不能證明活著,往下也不能證明危險。 新套件本來就沒人下載,而搶註名稱的套件靠蹭流量,數字幾天內就能衝上去。

還有一種更難抓的。我問「Redis stream 的 consumer group 怎麼做到 exactly-once」,它回 bullmq:很紅、維護良好、下載數漂亮,每一個訊號都是綠的。問題是 bullmq 是 Redis 上的工作佇列,不是 Redis Streams 的 consumer group 實作,它答的不是我問的那題。

存在、可信、答錯。這三件事互相獨立。

這裡面有一半可以問 AI:等一下跑 npm audit 會噴出一串公告,那些公告的觸發條件它整理得比你快。但「這個名字是不是它自己編的」不能問它,問 AI 等於問幻覺的來源本人。

註冊處那一趟只有你能跑,一個 HTTP 請求,回 200 就是有、404 就是沒有;回別的(連不上、被代理擋掉)不算答案,那種情況等一下講。驗名字用唯讀查詢,不要用 npm install 去試,裝成功的那一刻你就中了,那正是 unused-imports 進去的方式。哪天一個名字從 404 變成 200,是有人把它註冊走了,那時候更不能裝。

鎖得住版本,鎖不住第一次裝進來的東西

下一步是把裝進來的東西釘住。AI 給你的、你審查的都是 package.json 那一行,而那一行後面拖著幾個,它不會講。先看同一個相依差一個 ^

express "^4.19.2 " → 實際鎖到 4.22.2   lockfile 裡 68 個套件  弱點 0 則(critical 0 / high 0 / moderate 0 / low 0)
express "4.19.2  " → 實際鎖到 4.19.2   lockfile 裡 68 個套件  弱點 7 則(critical 0 / high 3 / moderate 1 / low 3)

這不代表釘版本比較危險。npm audit 報的是「你鎖住的那份 lockfile 現在對得上幾則公告」,浮動那邊的 0 是今天的 0,同一份 package.json 明天可能就不是。

那個「弱點 7 則」數的是有公告的套件數,不是公告則數。每一則去重來數是 11 則,散在 body-parsercookieexpresspath-to-regexpqssendserve-static 這七個上面。型態是我自己歸的:七則是 DoS 或 ReDoS,三則是 XSS,剩下那則是 cookie 連名稱、路徑、網域都吃得下界外字元。

則數不是「打不打得到我」的答案,要判可達得看觸發條件:path-to-regexp 那幾則要你的路由字串在同一段裡塞了多個參數,檢查一次路由表當場就能關掉。這就是分工的那一刀:觸發條件問 AI,「我的路由長不長那個形狀」只有你能答。它整理完還是要回公告原文與程式碼逐項核對。

然後看那個 68。你宣告了 1 個相依,lockfile 裡有 68 個套件。 另外 67 個你沒選過、沒看過名字、更沒查過是誰發的。前面那一整套查註冊處的動作,套用範圍是這 68 個。

lockfile 進版本庫之後,CI 要跟著把 npm install 換成 npm ci。官方文件寫得很直白(2026-08 查證,docs.npmjs.com):The project must have an existing package-lock.json or npm-shrinkwrap.json,沒有就直接失敗;跟 package.json 對不上的時候會 exit with an error, instead of updating the package locknpm install 遇到對不上則是安靜地幫你把 lockfile 改掉(2026-08 實測,npm 11.4.2):那份你審查過、進了版本庫的清單,在 CI 裡被換成別的,沒有人會告訴你。要自己試先看一句:If a node_modules is already present, it will be automatically removed before npm ci begins its install. 別在開發到一半的目錄隨手打。

但這兩樣的順序在後面:先查它是誰,再鎖版本。它們擋的是「下次裝的時候版本被人換掉」,擋不了「第一次裝進來的就是那個東西」。unused-imports 就是這樣進去的:那個名字從來不是真套件,lockfile 第一次記下的就是它;npm audit 在公告出來之前也不會響,那則公告是 2025-10-27 才發的,而 Koi 說這批活動同年 8 月就開始了。鎖住毒藥,只是保證每次都吃同一份。

而 PhantomRaven 連這句都站在外面。lockfile 記的是註冊處上那包檔案的版本與摘要,同一份 lockfile 每次裝到的位元組一樣,是毒藥也一樣;那種掛 HTTP 網址的相依連這層都繞過去,Koi 的原話是 Not cached. Not versioned. Not locked. Fresh.(2026-08 查證):每次安裝都從攻擊者的主機重抓一份,他要在哪一次換內容由他決定。

一個看起來很有說服力的綠燈

裝完之後我順手跑了 npm audit signatures,那 68 個套件全部通過:68 packages have verified registry signatures

這一行只講註冊處簽章。provenance 是分開統計的,我這次跑出來沒有那一行,所以那 68 不是「兩種證明都有」。

官方文件說它驗兩樣(2026-08 查證):註冊處的簽章,還有 The audit signatures command will also verify the provenance attestations of downloaded packages.。簽章被簽的內容寫得很死:${package.name}@${package.version}:${package.dist.integrity},用註冊處自己的金鑰簽,公鑰掛在 registry-host.tld/-/npm/v1/keys。名字、版本、檔案摘要,就這三樣。provenance 再往前一步,證明它是從哪一個原始碼儲存庫、哪一條建置工作流程建出來的。

兩樣都沒有回答發的人有沒有惡意。被簽的內容裡根本沒有「這包東西在做什麼」,所以一包惡意套件只要由發布者本人走正常流程送上去,照這個定義一樣拿得到一枚驗得過的簽章。

前面那個 unused-imports 那枚簽章綠不綠,我不知道,也不打算用推的:那個版本已經下架,而它真正的內容掛在一個 HTTP 網址上。

前兩天那兩個綠燈,是你沒動手的時候也會亮:Day 5 那個 <script> 不執行,修之前打進去也不會跳;Day 6 那個「容器裡讀不到」,腳本根本沒跑起來也會讀不到。那種綠燈拆得穿,修之前先讓它紅一次就好。

今天這個不一樣。它是對方動了手它還是亮,而我手上沒有一包受控的惡意樣本,這條邊界我沒實測過。倒是 npm 官方文件自己把界線寫明了(2026-08 查證,docs.npmjs.com):When a package in the npm registry has established provenance, it does not guarantee the package has no malicious code.

但它不是假綠燈,它是一個真的驗證,只是驗的是另一件事:這包檔案從註冊處送到你手上有沒有被換過。它不是惡意程式檢查。 前兩天那條規矩到今天要多長一截:除了問「事情沒做的時候它會不會也綠」,還要問它綠的那一題,是不是我在問的那一題

你自己跑一次

指令在下面,沒有一行會動到你的專案。

git clone https://github.com/cyh7789/ai-security
cd ai-security/recipes/07-does-it-even-exist
git checkout b16663e244792caff92d31acef9fb8ec1d3c1da7        # 釘在我審過的這一版,預設分支之後會往前跑
less check-pkgs.sh lockfile-demo.sh verify.sh   # :n 換下一支,q 離開

三支都看過再跑,不要只讀第一支。check-pkgs.shcurlnodelockfile-demo.shnpmnode,第三行三個都要:

bash check-pkgs.sh express zod 你手上那些名字
bash lockfile-demo.sh        # 上面那個 ^ 的對照
bash verify.sh 5

第三行是 npm ci 與簽章那兩段,它會真的裝,在 mktemp 目錄裡拉約 3.9 MB。裡面的 npm ci 自己帶 --ignore-scripts,你的相依跑不到 preinstallpostinstall;只有一輪故意不帶旗標,先證明那顆自己捏的探針寫得出檔,「帶了就沒寫」才不是永遠都綠。這一節在我這台五條綠(2026-08-07,npm 11.4.2)。

名字可以直接貼整串,npm install express@4.19.2 後面那個版本腳本會自己切掉。

lockfile-demo.sh 會清掉自己開的暫存目錄,但註冊處的中繼資料還是會寫進 ~/.npm/_cacache

check-pkgs.sh 全程只送 GET,開頭會先問一個一定在的、再問一個一定不在的,兩邊都答對才往下走。「查不到」有兩種:註冊處說沒有,或你根本沒問到它(網路斷了、代理擋掉),後者會讓你把真套件當成幻覺。對照擋不住的那一種,mutations.sh 裡有。檢查沒跑,跟檢查跑完沒事,畫面上不能長得一樣。

Python 專案換成查 PyPI 的 JSON API、pip-audit 代替 npm audit,我在一個乾淨的 venv 裡跑過,一樣是存在回 200、不存在回 404。

收工前

你手上多了三樣。

lockfile 進了版本庫,CI 從 npm install 換成 npm ci。這兩件事加起來的意思是:版本清單變成一份會被審查的檔案,而不是每次跑都重算一次的結果。

一份相依審視紀錄。把 check-pkgs.sh 的輸出存成檔案、寫上日期,跟 Day 2 那份祕密位置清單、Day 4 那份可貼清單放在一起。它的用途不是今天,是三個月後某個套件出事的時候,你查得到自己當初看到的是什麼。欄位先記著:名字、幾個維護者、多久沒動、你哪一天查的。第二欄只到人數,那是註冊處給的維護者清單,不是那一版的實際發布者,「誰發的」這一題腳本沒有回答你。明天你會拿同一張表去填另一批東西,欄位換成誰寫的、它碰得到什麼、什麼時候裝的。

還有一個數字:67。那是你這個專案裡,你沒有選過的相依數量。你今天不用把它們查完,但從今天起你知道它在那裡。

攻擊集停在 2 條,今天沒動。你手上有兩個數字了:一個是你擋得住的攻擊,一個是你還沒查過的東西。

明天

套件至少還算有名有姓。它躺在 package.json 裡,有註冊處查得到、有 lockfile 釘得住、有 npm audit 掃得到。明天那批東西,這三樣得一個一個問。

你裝一個 MCP server 或一個 skill,它走 npm、PyPI、容器映像或釘死的 git commit 的話,該鎖的鎖得住、該掃的掃得到,今天這一套原封不動搬過去;而另一種只給你一句「把這段設定貼進去」,那就沒有註冊處可查、沒有 lockfile 記你裝的是哪一版、也沒有 audit 掃。同一批東西裡兩種都有,你得先知道自己手上那個是哪一種。

權限那邊也不是「一定更大」。npm 的生命週期腳本一樣能用你的身分碰檔案、開 shell、讀你的憑證,今天已經看過一次了。差的是形態:那是安裝那幾秒鐘的事,而 MCP server 一裝上去就一直開著,坐在你跟模型中間,直接接收工具呼叫。

明天第一步是把它們列出來。列的過程你會發現一件事,就是你其實說不出它們各自碰得到什麼。


上一篇:Day 6|AI 其實有能力控制你整台電腦

今天這三支腳本:recipe 07|範例專案:github.com/cyh7789/ai-security


上一篇
Day 6|AI 其實有能力控制你整台電腦
下一篇
Day 8|你裝的那幾個 MCP,碰得到什麼
系列文
你該防的不是駭客,是你自己的 AI:在本機驗證你的防線18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言