2026 年每日通報的 CVE 數量比前一年多 131%。

PHP 安全

PHP 安全:在程式碼、Composer 相依套件與建置中防範 PHP 漏洞

保護 PHP 需要顧及三個層面。在您撰寫的程式碼中:驗證輸入、使用參數化查詢與框架的跳脫機制,並且絕不對不受信任的資料執行 unserialize。在您依賴的 Composer 套件中:執行 composer audit、提交 composer.lock,並封鎖不安全的版本。在安裝這些套件的 CI 中:記錄實際安裝了什麼,並限制建置可以連線的對象。多數 PHP 安全指南只談到第一層,本指南則涵蓋全部三層。

更新日期

  1. 您的程式碼 注入、XSS、反序列化、檔案處理、工作階段 框架預設值、程式碼審查、PHPStan、Psalm、Semgrep
  2. 您的 Composer 相依套件 已知漏洞、已棄用與惡意的套件、lockfile 偏移、安裝時執行的程式碼 composer audit、Composer 政策、Dependabot 或 Renovate
  3. 您的建置 CI 實際安裝並執行了什麼、從哪裡取得、能連到哪裡 鎖定版本的 actions、最小權限權杖、對外連線政策、建置時 SBOM
PHP 安全的三個層面,以及各層適用的工具

本頁內容:PHP2Shell、至今仍然重要的 PHP 漏洞、設定與受支援的版本、靜態分析、Composer 相依套件安全、PHP 供應鏈攻擊、CI 建置、SBOM,以及各層面的工具。

PHP2Shell:現在就該修補的 PHP 漏洞

2026 年 10 月 21 日,PHP 專案發布了 PHP 8.5.12、8.4.27、8.3.36 與 8.2.35,修正 PHP 本身兩個可能導致未經驗證遠端程式碼執行的重大漏洞。兩者在 CVSS v4.0 下皆評為 9.2 分,合稱 PHP2Shell:

  • CVE-2026-109618:記憶體不足的處理可能在請求資料僅部分初始化時中斷 PHP,之後的請求清理作業可能將無效資料視為有效。
  • CVE-2026-109619:屬於某個請求的狀態,可能在其記憶體釋放後仍然存在,而同一個 worker 處理的下一個請求可能會使用它。

PHP 的資安公告指出,所有處理網頁請求的安裝環境都有風險,包括 PHP-FPM、搭配 mod_php 的 Apache、php-cgi、FrankenPHP 與 LiteSpeed,其中 mod_php 的風險最高。PHP 8.1 及更舊的版本不會從 PHP 專案取得新版本。不過,有鑑於嚴重程度,PHP 資安團隊為自行從原始碼建置 PHP 或沒有 LTS 廠商的使用者,發布了 PHP 7.4、8.0 與 8.1 的最小化原始碼向後移植修正,部分發行版與 LTS 廠商也提供各自的向後移植修正。應採取的行動:

  • 升級到已修正的版本。若使用 8.1 或更舊的版本,請升級到受支援的分支、採用您廠商的向後移植套件,或在自行建置 PHP 時套用官方的原始碼向後移植修正。
  • 重新建置每個內含 PHP 的容器映像檔並重新部署。舊的映像檔標籤在被取代之前會繼續執行舊版 PHP,因此請檢查實際部署的內容,而不只是儲存庫中的內容:docker run --rm IMAGE php -v。
  • 若今天無法升級,請套用 PHP 資安公告中的暫時緩解措施,例如 session.upload_progress.enabled=0。請在 php.ini、FPM pool 或在請求解析前就會套用的伺服器設定中設定,並重新啟動每個 worker。不要依賴 ini_set() 或 FPM 的 .user.ini 來設定:資安公告對兩者都提出警告。PHP 明確表示,這些緩解措施能降低曝險,但無法讓安裝環境變得安全。

請注意 composer audit 在這裡無法告訴您的事:PHP2Shell 存在於執行環境中,而不是 Composer 套件中,因此它永遠不會出現在 composer.lock 中。您執行的 PHP 來自您的基礎映像檔或作業系統套件,也就是本指南的第三層。版本對照表、時間軸,以及檢查每個映像檔標籤的腳本,請參閱 PHP2Shell:要修補什麼,以及如何找出每個有漏洞的 PHP。

至今仍然重要的 PHP 漏洞

PHP 應用程式中的漏洞類型,十年來幾乎沒有改變。改變的是 Laravel、Symfony 與現代 PHP 預設就能防堵其中大多數,因此真正的錯誤如今集中在程式碼繞過這些預設值的地方。注入在 OWASP Top 10:2025 中排名 A05,而新進榜的 A03 軟體供應鏈失效(Software Supply Chain Failures),正是本指南後半部的主題。

漏洞 在 PHP 中的表現 修正方式 Laravel 與 Symfony
SQL 注入 請求資料被串接進 SQL,包括原始查詢的逃生口 使用綁定參數的 PDO 預備陳述式 Eloquent 與 Doctrine 會綁定參數;whereRaw()、DB::raw() 與串接的 DQL 則不會
跨站腳本攻擊(XSS) 使用者輸入未經跳脫就輸出到 HTML 在輸出時以 htmlspecialchars() 或樣板引擎跳脫 Blade 的 {{ }} 與 Twig 預設會跳脫;{!! !!} 與 |raw 則不會
跨站請求偽造(CSRF) 會變更狀態的表單或 AJAX 端點,接受任何帶有工作階段 Cookie 的請求 在每個 POST、PUT、PATCH 與 DELETE 上檢查每個工作階段專屬的權杖,並搭配 SameSite Cookie Laravel 會在 web 路由上檢查權杖(表單中使用 @csrf);Symfony Forms 預設會加入並檢查權杖;排除在檢查之外的路由則沒有任何防護
透過 eval() 的程式碼注入 請求資料,或使用者可編輯的樣板或公式,進入 eval() 不要使用 eval();將輸入對應到一組固定的操作,或針對所需的運算式使用剖析器 沒有任何框架能保護您自己的 eval() 呼叫
物件注入 對攻擊者可寫入的 Cookie、請求資料或快取項目執行 unserialize() 外部資料使用 JSON;若必須反序列化,請拒絕物件 沒有任何框架能保護您自己的 unserialize() 呼叫
檔案引入與上傳 由輸入組成的路徑被傳給 include;上傳檔案以用戶端的檔名存放在網站根目錄下 以允許清單限制可引入的檔案;將上傳檔案以產生的名稱存放在網站根目錄之外 Laravel 的 store() 會產生檔名;請在伺服器端驗證類型與大小
型別雜耍(type juggling) 以 == 比較權杖或雜湊值,而 "0e123" == "0e456" 的結果為 true 使用 ===,機密使用 hash_equals(),密碼使用 password_verify() 框架的輔助函式會安全地比較;您自己的比較則要自行負責
脆弱的工作階段與 Cookie 工作階段 ID 可被 JavaScript 讀取、透過 HTTP 傳送,或在登入前後沿用 使用 Secure、HttpOnly 與 SameSite Cookie、嚴格模式,並在登入後產生新的工作階段 ID 兩者都會在登入時重新產生工作階段 ID
伺服器端請求偽造(SSRF) 對使用者提供的 URL 使用 file_get_contents() 或 cURL,包括 file:// 與 phar:// 等包裝器 以允許清單限制協定與主機、封鎖內部位址,並在未使用時關閉 allow_url_fopen 沒有預設防護
命令注入 輸入被傳給 exec()、shell_exec()、system() 或 proc_open() 避免使用 shell;以陣列傳遞參數,或以 escapeshellarg() 逐一跳脫 Symfony Process 與 Laravel 的 Process facade 接受參數陣列
大量指派(mass assignment) User::create($request->all()) 讓請求可以設定 is_admin 列出可填入的欄位,或只傳入已驗證的輸入 Eloquent 模型預設受到保護;$guarded = [] 會關閉這項保護

SQL 注入:有漏洞與已修正的寫法

// Vulnerable: the id goes straight into the SQL string
$rows = $pdo->query("SELECT * FROM orders WHERE id = " . $_GET['id']);

// Fixed: a prepared statement with a bound parameter
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = ?');
$stmt->execute([$_GET['id']]);
$rows = $stmt->fetchAll();

// Laravel: whereRaw() takes SQL as written, where() binds the value
Order::whereRaw("status = '" . $request->input('status') . "'")->get(); // vulnerable
Order::where('status', $request->input('status'))->get();             // fixed

XSS:有漏洞與已修正的寫法

<?php // Plain PHP: escape on output
echo '<p>' . $comment . '</p>';                                              // vulnerable
echo '<p>' . htmlspecialchars($comment, ENT_QUOTES, 'UTF-8') . '</p>';    // fixed ?>

{{-- Blade: {{ }} escapes, {!! !!} prints raw HTML --}}
<p>{!! $comment !!}</p>   {{-- vulnerable --}}
<p>{{ $comment }}</p>     {{-- fixed --}}

{# Twig: autoescape is on, |raw turns it off #}
<p>{{ comment|raw }}</p>  {# vulnerable #}
<p>{{ comment }}</p>      {# fixed #}

物件注入:有漏洞與已修正的寫法

// Vulnerable: an attacker-controlled cookie becomes PHP objects, and any
// class with a __wakeup() or __destruct() method in vendor/ is a gadget
$prefs = unserialize($_COOKIE['prefs']);

// Fixed: JSON carries data, not objects
$prefs = json_decode($_COOKIE['prefs'], true, 512, JSON_THROW_ON_ERROR);

// If you must read serialized data, refuse objects
$prefs = unserialize($data, ['allowed_classes' => false]);

Phar 反序列化是一種變體:對 phar:// 路徑執行檔案操作時,會反序列化封存檔的中繼資料。PHP 8.0 縮小了這個攻擊面:開啟 phar 時,其中繼資料 不再被反序列化,只有在程式碼呼叫 getMetadata() 時才會。這又是一個該告別 PHP 7 的理由。

檔案引入:有漏洞與已修正的寫法

// Vulnerable: ?page=../admin/reset reaches any .php file on the disk
include __DIR__ . '/pages/' . $_GET['page'] . '.php';

// Fixed: only names on an allow-list can be included
$pages = ['home' => 'home.php', 'pricing' => 'pricing.php'];
$file = $pages[$_GET['page'] ?? 'home'] ?? 'home.php';
include __DIR__ . '/pages/' . $file;

設定與執行環境強化

即使應用程式本身安全,仍可能透過執行環境外洩資訊。以下 php.ini 設定依循 OWASP PHP Configuration Cheat Sheet:

設定 正式環境的值 原因
display_errors Off(並設定 log_errors = On) 堆疊追蹤會顯示路徑、查詢,有時甚至包括機密
expose_php Off 不再於回應標頭中公開確切的 PHP 版本
allow_url_include Off 防止 include 遠端 URL;自 PHP 7.4 起已棄用
allow_url_fopen 若應用程式不以檔案函式擷取 URL,則設為 Off 縮小經由 file_get_contents() 的 SSRF 攻擊面
disable_functions 例如 exec, shell_exec, system, passthru, proc_open, popen 讓已能執行程式碼的攻擊者無法使用 shell;只列出您的應用程式從不呼叫的函式
open_basedir 應用程式目錄 限制 PHP 檔案函式可開啟的路徑;這是一道護欄,而非隔離邊界
工作階段 Cookie session.cookie_secure = 1、session.cookie_httponly = 1、session.cookie_samesite = Lax 或 Strict、session.use_strict_mode = 1 避免工作階段 ID 透過一般 HTTP 傳送或被 JavaScript 存取,並拒絕非伺服器核發的 ID
file_uploads 若應用程式不接受上傳,則設為 Off 移除一整條輸入路徑

使用受支援的 PHP 版本

根據 php.net,PHP 為每個分支提供兩年的積極支援,之後兩年僅提供資安修正。PHP 8.1 已於 2025 年 12 月 31 日終止支援,PHP 8.2 則將於 2026 年 12 月 31 日跟進。終止支援的分支不會再從 PHP 專案取得新版本,PHP2Shell 正是讓 8.1 使用者陷入這種處境:他們唯一的官方修正是原始碼向後移植。

  • PHP 8.1 終止支援, 已於 2025年12月31日 結束
  • PHP 8.2 僅提供資安修正, 至 2026年12月31日
  • PHP 8.3 僅提供資安修正, 至 2027年12月31日
  • PHP 8.4 積極支援, 至 2026年12月31日
  • PHP 8.5 積極支援, 至 2027年12月31日
PHP 受支援的版本(日期來自 php.net)

Laravel 與 Symfony 的正式環境設定

  • 關閉除錯。Laravel 設定 APP_DEBUG=false,Symfony 設定 APP_ENV=prod 與 APP_DEBUG=0。正式環境中的除錯頁面會顯示設定與環境變數值。
  • 金鑰與機密。將 APP_KEY(Laravel)與 APP_SECRET(Symfony)排除在儲存庫之外,若外洩請立即輪替。Laravel 的 APP_PREVIOUS_KEYS 讓您輪替金鑰時不必將所有使用者登出。
  • 受信任的 Proxy。在負載平衡器之後,請列出您信任的 Proxy(Laravel 的 trusted proxies 中介軟體、Symfony 的 framework.trusted_proxies),讓用戶端無法以 X-Forwarded-* 標頭偽造其 IP 位址或 HTTPS。

PHP 的靜態分析(SAST)

靜態分析會在不執行程式碼的情況下讀取程式碼。三款開源工具即可滿足大多數 PHP 團隊的需求:

  • PHPStan 以逐步提高的嚴格等級找出型別錯誤與程式錯誤。它不是資安掃描工具,但許多資安錯誤都始於型別混淆或未檢查的 null。
  • Psalm 加入了污染分析(taint analysis):它追蹤資料從 $_GET 與 $_COOKIE 等來源,流向 echo、include 與 SQL 查詢等匯集點,並回報 TaintedSql 與 TaintedHtml 等問題。目前版本的 Psalm 會在型別檢查時一併執行污染分析;--taint-analysis 可在單次執行中啟用,--report=results.sarif 則會寫出供 GitHub code scanning 使用的 SARIF。
  • Semgrep 將 PHP 列為正式支援(generally available)的語言,提供針對常見 PHP 錯誤的社群規則,其商業版本則提供跨函式的資料流分析。

SAST 在舊版 PHP 上雜訊很多,原因出在結構上:動態引入、可變變數、__get() 與 __call() 等魔術方法,以及透過容器與 facade 傳遞資料、分析工具無法追蹤的框架慣例。工具要不是用猜的而回報誤判,就是直接放棄而漏掉真正的資料流。

在舊程式碼庫上導入時,最好採用棘輪式做法。將目前的檢測結果記錄為基準(vendor/bin/phpstan analyse --generate-baseline 會寫出 phpstan-baseline.neon;Psalm 則有 --set-baseline),只在出現新的檢測結果時讓建置失敗,並在逐步消化基準的同時提高等級。我們從擁有長期 PHP 程式碼庫的團隊聽到,AI 自動修正在相依套件升級上表現良好,在程式碼檢測結果上則表現不一,因此請像審查其他變更一樣仔細審查針對 SAST 檢測結果產生的修正。SAST 與 SCA 回答的是不同的問題;請參閱 SAST vs SCA。

Composer 相依套件安全

一個 Laravel 或 Symfony 應用程式會安裝數十個 Composer 套件,其中大多數是 遞移相依套件。Composer 在最近幾個版本中加入了實質的資安功能;本節依據目前的版本 Composer 2.11 撰寫。

composer audit

composer audit 會將您已安裝的套件,與 Packagist.org API 提供的資安公告比對,該 API 彙整了 GitHub Advisory Database 與 FriendsOfPHP/security-advisories 等來源的公告。它也會回報已棄用的套件,並自 Composer 2.10 起回報被標記為惡意軟體的套件版本。所有選項請參閱 Composer 文件。

composer audit                      # audit what is installed in vendor/
composer audit --locked             # audit composer.lock, whatever is in vendor/
composer audit --no-dev             # skip require-dev packages
composer audit --direct             # direct dependencies only (Composer 2.11)
composer audit --format=json        # table, plain, json or summary
composer audit --abandoned=report   # ignore, report or fail on abandoned packages
composer audit --ignore-severity=low

自 Composer 2.10 起,沒有問題時結束代碼為 0,任何項目未通過稽核時則為 1,因此不需剖析輸出就能作為 CI 關卡。已棄用的套件預設會導致稽核失敗。

已接受的檢測結果應連同理由寫在 composer.json 中,而不是放在沒人記得的 CI 旗標裡。Composer 2.10 已將大部分舊的 audit 設定改為 policy 區塊:

{
    "config": {
        "policy": {
            "advisories": {
                "ignore-id": {
                    "CVE-1234": "Not affected: we never call the vulnerable parser."
                }
            },
            "abandoned": {
                "audit": "report"
            }
        }
    }
}

舊的設定鍵(audit.ignore、audit.abandoned、audit.block-insecure)在對應的 policy 區段不存在時仍可作為備援,但已被棄用。請一次遷移所有與資安公告相關的設定:一旦 policy.advisories 存在,所有與資安公告相關的 audit.* 設定鍵都會被忽略。

封鎖不安全的版本

自 Composer 2.9(2025 年 11 月)起,composer update 與 composer require 會拒絕具有資安公告的套件版本。這原本是 audit.block-insecure 設定,現在則是預設啟用的 policy.advisories.block。Composer 2.10 加入了依據 Packagist 惡意軟體資訊來源進行的惡意軟體封鎖,這也適用於 composer install。Composer 2.11 加入了選用的 cooldown 政策,可在您選擇的期間內(例如 "7 days")暫緩採用新發布的版本,因為許多惡意版本會在數小時或數天內被發現並移除。

在 CI 中有兩個細節很重要。第一,資安公告封鎖是在解析版本時套用:資安公告發布後,已經在 composer.lock 中的版本仍會透過 composer install 安裝,因此您仍需要 composer audit 作為關卡。第二,--no-blocking 會在單一指令中關閉所有政策,值得在您的管線中搜尋一下。

在 Composer 2.9 之前,封鎖不安全版本的常見做法是 roave/security-advisories,這個套件依據 GitHub Advisory Database 與 FriendsOfPHP,對每個已知有漏洞的版本宣告 conflict。它仍然有效,在較舊的 Composer 版本上也仍有幫助;在 Composer 2.9 或更新版本上,內建的封鎖功能不需要額外套件就能涵蓋相同的資安公告。

Lockfile 紀律

  • 應用程式請提交 composer.lock,讓每個環境都安裝相同的版本。
  • 在 CI 與部署中使用 composer install,絕不使用會即時解析新版本的 composer update。
  • 以 --no-dev 安裝正式環境的建置,讓測試工具永遠不會被交付。
  • 執行 composer validate --strict:當 composer.lock 與 composer.json 不一致時,它會失敗。

Lockfile 記錄的是 Composer 解析的結果。請參閱 lockfile 不會記錄的內容。

安裝時執行的程式碼:外掛、腳本與自動載入的檔案

Composer 執行相依套件程式碼的地方比多數團隊預期的還多,這使它成為 PHP 版的 npm 安裝腳本:

  • 外掛在 Composer 本身內部執行。自 Composer 2.2 起, allow-plugins 設定必須列出每個允許執行的外掛,且預設不允許任何外掛。請將該處新增的項目,視同一個具有 shell 存取權的新相依套件。
  • 腳本只會從您的根目錄 composer.json 執行,不會從相依套件執行。但它們往往會啟動您的應用程式。
  • 自動載入的檔案會在每次載入自動載入器時執行。在標準的 Laravel 應用程式中, composer install 最後會執行 php artisan package:discover,而它會載入自動載入器,因此相依套件自動載入的 files 會在安裝期間於您的 CI 作業中執行。

如果作業不需要外掛或腳本,--no-plugins --no-scripts 可避免相依套件的程式碼在安裝期間執行;之後再明確執行您需要的步驟。

儲存庫來源與相依性混淆

Composer 可以從 Packagist.org、Private Packagist 等私有 Composer 儲存庫、VCS 儲存庫(Git URL)與本機路徑安裝套件。混用私有與公開來源,正是 相依性混淆的溫床:如果您的私有套件名稱也存在於 Packagist.org 上且版本更高,您會拿到哪一個?在 Composer 2 中, 每個儲存庫預設都是 canonical:一旦 Composer 在某個儲存庫中找到套件,就不會再往清單下方尋找,而 Packagist.org 永遠排在最後。在儲存庫上設定 "canonical": false 會移除這項保護,因此只有在刻意為之時才這麼做。VCS 儲存庫會拉取 Git 主機為某個標籤提供的任何內容,因此請在 lockfile 中以 commit 鎖定它們,並審查其變更。

已棄用與無人維護的套件

舊有的 PHP 系統常帶有維護者早已離開的套件。當維護者宣告時,Packagist 會將套件標記為已棄用,通常也會指出替代套件,而 composer audit 預設會因此失敗。不過,許多無人維護的套件從未被正式棄用。對於任何處理輸入、身分驗證或密碼學的套件,請查看其最後發布日期,並在漏洞逼迫您之前就規劃替換。

測試不足時如何安全更新

最困難的情況,是一個大型舊有應用程式含有有漏洞的相依套件,而測試又很少。有效的做法:

  1. 更新引入有漏洞套件的那個套件。大多數檢測結果來自遞移相依套件;執行 composer why vendor/package 查看相依鏈,並先在版本限制範圍內更新上游套件。
  2. 一次只更新一項。composer update vendor/package --with-dependencies 能讓變更小到足以在 composer.lock 的差異中審查。
  3. 先對關鍵路徑做冒煙測試。登入、結帳、主要 API 端點:少數幾項端對端檢查,就能抓出大部分升級造成的問題。
  4. 針對升級影響的部分加入特性測試。記錄程式碼目前的行為,讓您在升級改變它時能察覺。
  5. 分階段推出,並在推及所有客戶之前觀察錯誤率。

composer audit 看不到的東西

Lockfile 是一份計畫,而不是建置的紀錄。composer audit 讀取的是 vendor/ 或 composer.lock,因此它看不到:

  • PHP 本身及其擴充套件,這些來自您的基礎映像檔或作業系統套件(PHP2Shell 就是一例)。
  • 安裝在 Docker 映像檔中的 PECL 擴充套件與系統函式庫。
  • 您的 CI 以 curl 取得的工具、它執行的 actions,以及外掛或腳本下載的二進位檔。
  • 尚未有資安公告或惡意軟體標記的惡意程式碼。

npm audit 與 pip-audit 在各自的生態系中也有相同的限制。

PHP 供應鏈攻擊

以下四起事件說明了針對 PHP 的供應鏈攻擊實際落在哪裡,以及什麼原本可以發現它們。

2021 年 3 月:對 php-src 的惡意 commit

2021 年 3 月 28 日,有人以兩位核心開發者的名義,將兩個惡意 commit 推送到 php-src 儲存庫。PHP 團隊 宣布將停止運作自家的 Git 伺服器,改以 GitHub 儲存庫為正式來源。其 後續調查發現,這些 commit 是透過 HTTPS 以密碼驗證推送的,可能使用了外洩使用者資料庫中的憑證,而不是透過遭入侵的伺服器。什麼原本可以發現它:您自己的管線中沒有任何東西可以。它鎖定的是語言本身,這提醒我們 PHP 執行環境也是一個相依項目。

2021 年 4 月:Composer 命令注入(CVE-2021-29472)

Composer 處理 Mercurial 儲存庫 URL 的方式存在缺陷,可導致命令注入。 Packagist 表示,Packagist.org 與 Private Packagist 理論上可能透過特製的套件 URL 曝險,但認為並未遭到利用,並已在 Composer 2.0.13 與 1.10.22 中修正。什麼原本可以發現它:在開發人員的電腦與 CI 映像檔中保持 Composer 本身為最新版本。套件管理工具也是您供應鏈的一部分。

2023 年 5 月:Packagist 帳號遭接管

2023 年 5 月 1 日,一名攻擊者使用在其他地方外洩的密碼,登入四個不活躍的 Packagist.org 帳號,這些帳號共控制 14 個套件,攻擊者並將這些套件指向分叉的儲存庫。這些分叉只修改了描述,不含惡意程式碼。 Packagist 於隔天早上將其復原。什麼原本可以發現它:已提交的 composer.lock,因為 composer install 會使用 lockfile 中記錄的來源;以及審查 lockfile 差異中是否有非預期的 URL,這也是 Packagist 本身的建議。對維護者而言:使用不重複的密碼與雙重驗證。

2026 年:laravel-lang 與 intercom-php 的標籤遭劫持

2026 年,取得竊得存取權的攻擊者發布了既有套件的惡意版本。Packagist 在其 intercom/intercom-php(4 月 30 日)與 laravel-lang 套件(5 月 22 日)的 供應鏈安全最新說明中點名了這些套件。在 laravel-lang 事件中,版本標籤指向惡意分叉中的 commit,而惡意酬載是一個 src/helpers.php 檔案, 由 Composer 的自動載入器載入,會下載一個竊取雲端金鑰、SSH 金鑰與 .env 檔案的憑證竊取程式。Aikido 統計共有 233 個遭入侵的版本。什麼原本可以發現它:一旦被標記,Composer 2.10 的惡意軟體政策即使在 composer install 時也會封鎖這些版本;在那之前,冷卻期能把全新的版本擋在外面,對外連線政策能阻止酬載連到其伺服器,而每個建置的紀錄則能顯示哪些建置安裝了有問題的版本。

此後,Packagist 宣布會拒絕重新標記的穩定版本、發布記錄擁有權與標籤變更的透明度日誌,並自 2026 年 3 月起在 Packagist.org 上標記惡意軟體。

在 CI 中保護 PHP 建置

建置是您的程式碼、相依套件與機密交會之處,因此也是惡意套件能造成最大損害的地方。以下是一個用於正式環境建置的 GitHub Actions 工作流程:

name: build
on: [push, pull_request]

permissions:
  contents: read            # least privilege for GITHUB_TOKEN

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0
        with:
          php-version: "8.4"
          tools: composer:v2
          coverage: none

      - run: composer validate --strict

      - id: composer-cache
        run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
      - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          key: composer-${{ hashFiles('composer.lock') }}

      - run: composer install --no-dev --no-interaction --no-progress --optimize-autoloader
      - run: composer audit --no-dev
  • 最小權限權杖。permissions: contents: read 可阻止遭入侵的步驟使用 GITHUB_TOKEN 推送程式碼或發布版本。請參閱 GitHub 權杖權限。
  • 鎖定 actions 版本。完整的 commit SHA 無法像標籤那樣被移動;註解則讓 Dependabot 或 Renovate 仍能讀出版本。
  • 快取下載內容,而非 vendor/。快取 Composer 的下載快取,能讓每個作業中的 composer install 都依據 lockfile 執行。還原的 vendor/ 會被原封不動地信任,而快取可能跨工作流程遭到污染;請參閱 GitHub Actions 快取。
  • 記錄安裝了什麼。Lockfile 說明的是應該安裝什麼。從建置記錄的 SBOM 則說明實際安裝了什麼,包括 PHP 執行環境、作業系統套件,以及 Composer 以外的下載內容。請參閱 建置時 SBOM。
  • 限制建置可連線的對象。Composer 建置需要 Packagist、GitHub 與您的私有儲存庫。只允許這些對象的對外連線政策,能阻止憑證竊取程式送出它找到的東西。

為 PHP 專案產生 SBOM

標準的開源選項是 CycloneDX Composer 外掛。由於它是外掛,需要在 allow-plugins 中加入一個項目:

composer require --dev cyclonedx/cyclonedx-php-composer
composer CycloneDX:make-sbom --output-format=JSON --output-file=sbom.json

它讀取 composer.json,以及已安裝的套件或 composer.lock,因此能準確列出您的 Composer 相依套件,但僅止於此。PHP 執行環境、其擴充套件、基礎映像檔的 apt 或 apk 套件,以及作業下載的工具,都不在其中。建置時 SBOM 也會記錄這些內容,因為它是依據作業實際取得的內容,而不是 Composer 的檔案。關於格式以及 SBOM 應包含的內容,請參閱 什麼是 SBOM。

各層面的 PHP 安全工具

層面 工具 涵蓋範圍 遺漏之處
程式碼 PHPStan、Psalm、Semgrep 程式錯誤與型別錯誤、受污染的資料流、已知的不安全模式 有漏洞的相依套件、執行環境、建置
相依套件 composer audit、Composer 政策、roave/security-advisories、Dependabot 或 Renovate 已發布的資安公告、已棄用與被標記的套件、更新用的 pull request 您自己的程式碼,以及 composer.lock 以外的任何東西
映像檔 容器掃描工具 已建置映像檔中的作業系統與語言套件 建置取得但未保留的內容,以及沒人掃描過的映像檔
SBOM CycloneDX Composer 外掛 以 CycloneDX SBOM 呈現的 Composer 套件 Composer 以外的一切
建置 CRACI 每個 GitHub Actions 作業實際安裝的內容,來自 Composer 與其他所有來源,並長期監控 您的 PHP 程式碼(不提供 SAST);GitHub Actions 以外的 CI 系統

更多關於映像檔的內容:容器掃描。在所有層面上取得更少、更好的檢測結果: 如何減少漏洞雜訊。

PHP 安全與 CRACI

CRACI 是一個 GitHub Actions 執行器,會記錄每個作業實際安裝了什麼。將 runs-on 改為 craci,作業下載的每個 Composer 套件都會連同其套件 URL (pkg:composer/vendor/package)一起被記錄,同一個作業中來自作業系統套件管理工具、容器映像檔、Git 來源與其他下載的套件也是如此,包括從 CI 快取還原的套件。SBOM 中的每個元件都確實存在於建置之中。

CRACI 會將這些元件與已知漏洞比對,並在新的資安公告發布時,持續重新檢查您已交付內容的 SBOM,因此即使是明年才揭露的 CVE,仍能帶您找到包含它的建置。 政策關卡可以依檢測結果中止建置,包括含有特定 CVE 的建置,而對外連線政策則會限制作業可以連線的對象。

CRACI 不會掃描您的 PHP 程式碼:這是刻意的設計,不提供 SAST。程式碼層面請繼續使用 PHPStan、Psalm 或 Semgrep。 查看您的 PHP 建置安裝的每個套件。

CRACI 不適用的情況

  • 您透過 FTP 部署,或部署到沒有 CI 的共享主機。沒有建置可以記錄。請從 composer audit 與已提交的 composer.lock 開始。
  • 您的風險主要在自己的程式碼中,例如充滿原始 SQL 的舊有應用程式。SAST、審查與測試應優先進行。
  • 您在 GitHub Actions 以外建置,例如 GitLab CI 或 Bitbucket Pipelines。CRACI 目前支援 GitHub Actions;GitLab CI 與 Jenkins 已列入產品藍圖。

PHP 安全:常見問題

如何保護 PHP 程式碼?

從三個層面著手。在程式碼中,驗證輸入、使用預備陳述式、依賴框架的輸出跳脫,並且絕不對不受信任的資料執行 unserialize。在相依套件中,提交 composer.lock、在 CI 中執行 composer audit,並保持 Composer 對不安全版本的封鎖為啟用狀態。在建置中,鎖定 actions 版本、限制權杖權限、限制網路存取,並記錄作業安裝了什麼。

最常見的 PHP 漏洞有哪些?

SQL 注入、跨站腳本攻擊、跨站請求偽造、透過 eval() 的程式碼注入、透過 unserialize() 的物件注入、檔案引入與不安全的上傳、使用 == 的型別雜耍、脆弱的工作階段設定、透過 URL 包裝器的 SSRF、命令注入,以及大量指派。有漏洞的 Composer 相依套件與未修補的 PHP 版本(例如 PHP2Shell 漏洞),同樣是常見的資安事件成因。

最好的 PHP 資安掃描工具是哪一個?

沒有任何單一工具能涵蓋 PHP 安全。程式碼請使用 PHPStan、Psalm 或 Semgrep 等靜態分析工具,Composer 相依套件請使用 composer audit,映像檔請使用容器掃描工具,CI 實際安裝的內容則請使用建置時紀錄。

composer audit 如何運作?它會遺漏什麼?

它會將已安裝的套件(或搭配 --locked 時的 composer.lock)與 Packagist.org API 提供的資安公告比對,並回報已棄用與被標記為惡意軟體的套件。任何項目未通過稽核時,它會以 1 結束。它看不到 PHP 本身、PECL 擴充套件、映像檔中的作業系統套件,也看不到 CI 在 Composer 以外下載的任何東西。

如何在 CI 中封鎖不安全的 Composer 套件?

使用 Composer 2.9 或更新版本,它預設會在 update 與 require 時封鎖具有資安公告的版本(policy.advisories.block)。由於已鎖定的有漏洞版本仍會被安裝,也請在 composer install 之後執行 composer audit,並依其結束代碼讓作業失敗。絕不要在 CI 中傳入 --no-blocking。

Laravel 預設就安全嗎?

大致上是。Blade 會跳脫輸出、Eloquent 會綁定查詢參數、模型預設防範大量指派,且工作階段 ID 會在登入時重新產生。當程式碼使用 {!! !!}、以輸入呼叫 whereRaw()、設定 $guarded = []、在正式環境中設定 APP_DEBUG=true,或 APP_KEY 外洩時,這些預設值就會失效。

如何在測試很少的舊有 PHP 應用程式中更新相依套件?

更新引入有漏洞套件的上游套件,並使用 composer update vendor/package --with-dependencies 一次只更新一個套件。對關鍵路徑做冒煙測試、針對升級影響的部分加入特性測試,並分階段推出。

如何為 PHP 專案產生 SBOM?

CycloneDX Composer 外掛(cyclonedx/cyclonedx-php-composer)可透過 composer CycloneDX:make-sbom,依據您的 Composer 套件產生 SBOM。它只會列出 Composer 套件。建置時 SBOM 則還會記錄建置中的 PHP 執行環境、作業系統套件與其他下載內容。

什麼是 PHP2Shell?

PHP2Shell 是 CVE-2026-109618 與 CVE-2026-109619 的統稱,這是 PHP 本身兩個可能導致未經驗證遠端程式碼執行的重大漏洞。它們已在 2026 年 10 月 21 日發布的 PHP 8.5.12、8.4.27、8.3.36 與 8.2.35 中修正。請參閱 PHP2Shell。

查看您的 PHP 建置安裝的每個套件

在 CRACI 上執行一個 GitHub Actions 工作流程,並將其記錄的 SBOM 與您的 composer.lock 進行比較。

預約示範