Composer 生態系剛被打了一記重拳。2026 年 5 月 22 日 UTC 約 11:00 到 11:15 之間,一個拿到 Laravel-Lang GitHub 組織 push 權限的攻擊者,對四個被廣泛使用的 Composer 套件改寫 tag 參照,把 233 個歷史版本標籤重新指向一個惡意 fork 的 commit。當天下午第一批訊號才被 Aikido 與 Socket 的研究員偵測到,那時候依賴圖上超過 700 個歷史版本已經被毒化,未知數量的 CI pipeline 已經把 payload 跑完了。
Payload 是一支 5,900 行的 PHP 憑證竊取程式,以 autoload 檔案的形式註冊在 composer.json 裡,意思是它會在受感染應用程式處理的每一個 PHP request 都執行,不只在 install 時跑。它鎖定雲端金鑰、SSH 憑證、瀏覽器密碼、CI secrets。Packagist 之後已經把受影響版本下架,但暴露窗口很寬。如果你的團隊在過去 72 小時內任何一刻安裝過 laravel-lang/lang、laravel-lang/http-statuses、laravel-lang/attributes、或 laravel-lang/actions——或者你的 CI 在例行 composer update 拉過它們——你就是一個 incident。
下面是發生了什麼、為什麼這次攻擊是一個「類別」、以及這禮拜的七步應變劇本。
一、機制:Tag Rewrite 才是攻擊向量
多數對 Composer 或 npm 的供應鏈攻擊,做法是「發佈一個新的、惡意的版本」。防守方靠盯「沒預期會有新版的套件出現新版」抓到它們。Laravel-Lang 攻擊做的剛好相反:它改寫早就存在的版本的參照。對團隊已經用了兩年的套件、跑一個 composer install 鎖在 ^1.0,突然抓到的就不是規範性的 commit,而是 fork 控制的 payload。
技術上的利用點是 GitHub 對 fork 參照的寬鬆政策。一個 repo 裡的 tag 可以指向一個 fork repo 裡的 commit,只要 fork reachable。攻擊者用對 Laravel-Lang 組織的 push 權限,把四個套件的 tag 全部重新指向他控制的 fork 的 commit;在那個 fork 裡,src/helpers.php 已經被換成憑證竊取程式。Composer 的套件解析不分辨「來源 repo 的 commit」與「任何 reachable fork 的 commit」;tag 就是事實,而 tag 現在指向惡。
這種 pattern——透過 fork 參照的 tag 改寫——是這禮拜防守方該學會的類別。它同時打敗了 lockfile 釘版本(因為 lockfile 釘的是 tag,而 tag 被改寫了)和版本範圍稽核(因為沒有出現新版本)。真正有效的防守只有:以 commit hash 釘版本、install 時做簽章驗證、或在非 production 環境做分階段驗證。
二、Payload 到底幹什麼
這支憑證竊取程式是近幾年公開面向最完整的 PHP 惡意程式之一。15 個專職的 collector 模組,從一個 autoload.files 條目觸發,在應用程式服務的每一個 request 都會執行。模組大致按執行順序鎖定:雲端存取金鑰(AWS access key、GCP application default credentials、Azure token、DigitalOcean token);基礎設施 config(Kubernetes kubeconfig、Docker registry token、HashiCorp Vault token);開發者 secret(SSH 私鑰、.git-credentials、.env 檔);CI/CD secret(Jenkins、GitLab Runner、GitHub Actions);瀏覽器資料(17 種 Chromium 系瀏覽器存的登入、cookie、autofill、內部後台瀏覽歷史);密碼管理器(KeePass、1Password 本地 vault);以及加密資產(桌面 wallet、MetaMask、Slack token、Discord session 檔)。
全部回傳到攻擊者控制的端點。值得注意的設計選擇是:竊取程式跑在每一個 request 上,不是只在 install 時跑。一台週五下午拉了套件、之後沒重啟、整個週末繼續服務流量的伺服器,每服務一個頁面就洩漏一次憑證。
三、這禮拜的七步應變
如果你的團隊有任何暴露,按順序跑。如果不確定,停下來先跑第一步。
步驟一:找出暴露面。 在每一台伺服器、CI cache、開發機、已建構容器上跑 composer show laravel-lang/* --installed。如果有任何結果版本是 5/23 下架之前的,你必須假設那台機器有權存取的所有 credential 都已淪陷。
步驟二:輪換雲端金鑰。 AWS access key、GCP service-account key、Azure SPN secret、DO API token。就算你「只」在開發筆電裡有這個套件——竊取程式在哪台伺服器跑 request,就洩漏哪台的 credential。
步驟三:輪換 CI token。 GitHub Actions token、GitLab Runner 註冊 token、Jenkins API token、CI vault 裡任何部署 SSH key。
步驟四:輪換 SSH key。 開發機的使用者帳號 key 與 CI vault 裡的部署 key 都要。對所有金鑰式存取強制重新註冊。
步驟五:輪換資料庫與第三方 API credential。 應用程式能讀到的任何 .env 檔內容都算。這是最廣、也最痛的一類,但這是對的答案。
步驟六:稽核每一個 repo 的 Composer lockfile。 任何釘到受影響版本的 lockfile,都要對已知乾淨的來源重新產出。能的話,至少暫時把高信任度套件從 tag 釘版本切到 commit-hash 釘版本。
步驟七:把 dependency-installation 掃描引入 CI。 Socket、Snyk、Aikido、Phylum 等工具現在都提供 install 時掃描,可以偵測這種形狀的 tag 改寫。把掃描器設成「tag drift 就 fail build」而不是只 warn。
整個循環,對於有 5–10 台 production 伺服器加 CI 的小型 Laravel 工作室,大概是一天的工。對於管幾十個客戶 codebase 的代理商,至少抓兩天,並預設其中會有一個客戶演變成完整的鑑識專案。
四、更大的 pattern 與該採用的防守姿態
Laravel-Lang 攻擊是今年第三起利用「受信任維護者 push 權限」而非全新 CVE 的大型 Composer/npm 供應鏈淪陷。過去在別的場景有效的防守姿態——準時打 patch、看 CVE feed、被通知就輪換——對這個類別不夠。兩種姿態真的有用。
install 步驟的縱深防禦。 把 composer install 與 npm install 當作未受信任的程式碼執行。把它們跑在沒有 credential 的容器裡,不要在帶有雲端金鑰的開發筆電上跑,也不要在握有 production 部署金鑰的 CI runner 上跑。下一次 tag-rewrite 攻擊的爆炸半徑,會被壓縮到 install sandbox 裡。
Provenance 驗證。 Sigstore 式的簽署式 release 對 Composer 與 npm 都已經可上 production。Provenance 檢查在 install 時花幾秒,會抓到 Laravel-Lang 這次的 tag 改寫,因為惡意 commit 不是維護者簽的。這是接下來一季最重要的衛生投資;別讓「我們太忙」變成它不發生的理由。
社群維護、人手稀薄的套件,是現代 Web stack 的軟肋。Laravel-Lang 有 2–3 位活躍維護者,承擔被數十萬個 repo 依賴的套件。維護者時數與利用回報之間的不對稱是結構性的,會持續產生這類事件。對的應變在「依賴消費端」,不是期待維護者組織突然長出 security 團隊。
五、誠實的解讀
對於過去三年把越來越多開源放進應用程式 bundle 的實務工作者,Laravel-Lang 攻擊該觸發一次重新評估。釘版本不夠。install 後留審計軌跡不夠。唯一足夠的做法是 sandbox 整個 install,並在 install 時驗證 provenance。兩者在技術上已經可行兩年了;幾乎沒有團隊兩個都做。
成本效益的天平剛剛移動。兩個都做的成本是幾分鐘 CI 與一天的 pipeline 工。兩個都不做的成本,是慢慢累積的「我們上禮拜憑證有沒有洩漏?」直到某個禮拜二早上,答案是「有」。
我的觀點
我想對抗的敘事是:把這次跟過去 18 個月一連串類似事件並列、當「又一個供應鏈攻擊」。它的結構不同。Tag rewrite 是第一個打敗「lockfile 釘版本」這項——大多數團隊正是因為過去的供應鏈攻擊才採用的——防守機制的攻擊技術。軍備競賽已經跑過大多數代理商最後一次升級的防守線。停下來升級的時刻是現在,不是下一個事件之後。
我想對抗的另一個敘事是:「我們等 Laravel 官方安全公告再行動。」社群維護的 Composer 套件沒有中央權威。Laravel core 團隊不對 Laravel-Lang 負責;Packagist 的角色是配送、不是安全;Laravel-Lang 維護者已經在盡力,但他們是小團隊面對這個規模的事件。應變責任在應用程式擁有者手上,而應變窗口關得比官方管道動得還快。如果你在 Laravel stack 上、有任何東西可以失去,今天就跑那七步。
資料來源
- Laravel-Lang PHP Packages Compromised to Deliver Cross-Platform Credential Stealer — The Hacker News
- Supply Chain Attack Targets Laravel-Lang Packages with Credential Stealer — Aikido
- Laravel-Lang Compromised with RCE Backdoor Across 700+ Versions — Socket
- Supply Chain Storm: Over 700 Laravel Lang Versions Poisoned with Malicious RCE Backdoor — Security Online
- Hackers Compromised 233 Versions of Laravel-Lang Packages by Hacking 700 GitHub Repos — Cyber Security News