你以為getimagesize很安全?
摘要
這份研究報告對 PHP 核心的標準擴充功能 (ext/standard) 中發現的兩個重大記憶體安全漏洞提供了全面的技術分析。第一個漏洞編號為 CVE-2025-14177,是
getimagesize
函式中因不當的 stream chunk 管理所導致的堆積記憶體洩露。第二個漏洞是
iptcembed
函式中的堆積緩衝區溢位,源於 Time-of-Check to Time-of-Use (TOCTOU) 的缺陷以及在位元組串流處理過程中缺乏邊界檢查。此報告剖析了底層的 C 語言實作,探討了攻擊機制,並討論了高階語言直譯器中記憶體管理的廣泛影響。
1. PHP 核心安全性與 Zend Engine 簡介
PHP 仍然是現代網站開發的基石,其安全性通常透過應用程式層框架的角度來評估。然而,底層的 C 語言核心,特別是 Zend Engine 及其標準擴充功能,代表了一個關鍵的攻擊面。Zend Engine 負責 PHP 程式碼的完整生命週期,從詞法分析、語法分析到透過 Zend VM 執行
[1]
。在此架構中,Zend Memory Manager 處理 request-bound 的配置,而 Zend API 則允許像
ext/standard
這樣的擴充功能與引擎進行互動。
ext/standard
擴充功能尤其敏感,因為它透過
getimagesize
和
iptcembed
等函式處理不受信任的外部資料。這些 C 語言層級的實作漏洞可能會繞過高階語言通常應具備的記憶體安全保證,導致資訊洩露或任意程式碼執行等嚴重後果。
2. CVE-2025-14177:getimagesize 中的堆積記憶體洩露
getimagesize
函式設計用來取得影像檔案的尺寸與中繼資料。在處理 JPEG 檔案時,它可以提取 APP (Application) 區段,例如 EXIF 或 XMP 資料。CVE-2025-14177 漏洞源於 PHP 從 stream 中以多個 chunks 讀取這些區段時的邏輯錯誤。
2.1 技術根本原因分析
此缺陷位於
php-src/ext/standard/image.c
中的
php_read_stream_all_chunks
函式。該函式目的是從 stream 讀取指定長度的資料,如果資料超過內部 chunk 大小(預設為 8192 位元組),可能會跨多個讀取操作進行。
- /*
- * Vulnerable function in php-src/ext/standard/image.c
- * This function fails to advance the buffer pointer after each read operation.
- */
- static size_t php_read_stream_all_chunks(php_stream *stream, char *buffer, size_t length)
- {
- size_t read_total = 0;
- do {
- /*
- * BUG: The second argument 'buffer' is always the start of the allocated memory.
- * Subsequent reads overwrite the beginning of the buffer instead of appending to the end.
- * The correct call should use 'buffer + read_total' as the destination.
- */
- ssize_t read_now = php_stream_read(stream, buffer, length - read_total);
- /* Update the total count of bytes read from the stream */
- read_total += read_now;
- /* Check for unexpected end of stream or read errors */
- if (read_now < stream->chunk_size && read_total != length) {
- return 0;
- }
- } while (read_total < length);
- return read_total;
- }
如上述程式碼所示,
php_stream_read
呼叫總是使用 base
buffer
位址。如果某個區段為 9000 位元組,而 chunk 大小為 8192,則前 8192 位元組會寫入
buffer[0..8191]
。剩餘的 808 位元組接著會被寫入
buffer[0..807]
,覆蓋掉最初的資料。關鍵在於,從
buffer[8192..8999]
的記憶體範圍保持未初始化,其中包含堆積上先前存在的任何資料
[1]
。
2.2 影響與攻擊
當
getimagesize
將中繼資料回傳給 PHP 程式碼時,結果字串會包含損壞的區段資料以及洩漏的堆積記憶體。攻擊者可以透過將敏感資訊「Spraying」到堆積上,然後觸發此漏洞來讀回這些片段。下圖說明了記憶體損壞的程序:
Allocate 9000 bytes Buffer] --> B[Read Chunk 1:
8192 bytes] B --> C[Write to buffer
index 0 to 8191] C --> D[Read Chunk 2:
808 bytes] D --> E[Write to buffer
index 0 to 807 - OVERWRITE] E --> F[Result:
buffer 8192 to 8999
contains
UNINITIALIZED HEAP DATA]
3. iptcembed 中的堆積緩衝區溢位
iptcembed
函式用於將 IPTC 中繼資料嵌入 JPEG 影像。與
getimagesize
中的揭露錯誤不同,此漏洞是一個典型的堆積緩衝區溢位,源於對檔案大小的錯誤假設。
3.1 「量一次,讀到永遠」的缺陷
iptcembed
函式透過對輸入檔案呼叫
fstat()
來決定所需的輸出緩衝區大小。接著,它會根據回報的
st_size
配置一個緩衝區 (
spoolbuf
)。然而,它隨後會持續讀取輸入 stream 直到達到 End-Of-File (EOF),並將每個位元組複製到緩衝區中,而不進行進一步的邊界檢查
[1]
。
- /*
- * Vulnerable allocation logic in php-src/ext/standard/iptc.c
- * Uses fstat to determine buffer size, creating a TOCTOU window.
- */
- if (spool < 2) {
- /* Get file status to determine size */
- if (zend_fstat(fileno(fp), &sb) != 0) {
- fclose(fp);
- RETURN_FALSE;
- }
- /*
- * Allocate buffer based on sb.st_size.
- * If the file is a FIFO or grows later, this size is insufficient.
- * The allocation includes overhead for IPTC data and a small reserve.
- */
- spoolbuf = zend_string_safe_alloc(1, iptcdata_len + sizeof(psheader) + 1024 + 1, sb.st_size, 0);
- poi = (unsigned char*)ZSTR_VAL(spoolbuf);
- /* Initialize the allocated buffer with zeros */
- memset(poi, 0, iptcdata_len + sizeof(psheader) + sb.st_size + 1024 + 1);
- }
php_iptc_get1
函式執行了實際的寫入操作,進一步加劇了此漏洞。它會為從檔案讀取的每個位元組遞增指標
spoolbuf
(或
poi
),但從未驗證該指標是否已超過配置的記憶體邊界。
- /*
- * Vulnerable write operation in php-src/ext/standard/iptc.c
- * No bounds checking before writing to the buffer.
- */
- static int php_iptc_get1(FILE *fp, int spool, unsigned char **spoolbuf)
- {
- int c;
- /* Read one byte from the file pointer */
- c = getc(fp);
- if (c == EOF) return EOF;
- /*
- * BUG: Writing to the buffer and incrementing the pointer
- * without checking if we have reached the end of the allocated space.
- * This leads to a heap buffer overflow if input exceeds sb.st_size.
- */
- if (spoolbuf) *(*spoolbuf)++ = c;
- return c;
- }
3.2 透過 named-pipe (FIFOs) 進行攻擊
觸發此溢位的一個特別有效的方法是使用 named-pipe (FIFO)。對於 FIFO 而言,
fstat()
通常會回傳大小為 0。因此,
iptcembed
會配置一個極小的緩衝區。然而,攻擊者可以透過 FIFO 串流大量的資料。
php_iptc_read_remaining
函式將持續讀取並將這些資料寫入,直到 EOF,導致堆積上發生大規模的越界寫入
[1]
。
4. 比較分析:記憶體安全性 vs. 型別安全性
所討論的漏洞突顯了使用 C 語言的軟體中的一個根本挑戰:手動管理記憶體以及缺乏內建的安全性檢查。雖然像 PHP 這樣的高階語言提供了一抽象層,但它們仍然容易受到底層實作的缺陷影響。
有趣的是,這些記憶體安全問題在概念上與 PHP 其他區域中發現的型別安全缺陷有相似之處。例如,PHP 的鬆散型別系統歷史上曾導致認證繞過,其中數字字串的比較意外地評估為真 [2] 。在這兩種情況下,根本原因都是開發者的假設(認為緩衝區足夠大,或者某個變數是特定型別)與執行環境的實際行為之間的不匹配。
iptcembed
的缺陷也是一個典型的 TOCTOU (Time-of-Check to Time-of-Use) 漏洞。「檢查」(
fstat
呼叫)與「使用」(後續的讀取迴圈)之間存在一個時間窗口,在此窗口期間,資源的狀態(檔案大小)可能發生變化。此模式在系統安全性中反覆出現,強調任何外部狀態都必須被視為易變且不可信任。
5. 技術研究與緩解策略
對這些錯誤的研究強調了在 C 語言擴充功能中進行防禦性程式設計的必要性。對於
getimagesize
,修復方式涉及將緩衝區指標前移實際讀取的位元組數(
buffer += read_now
),以確保資料能被循序串接。對於
iptcembed
,解決方案需要隨著緩衝區增長動態重新配置,或者在讀取迴圈中嚴格執行初始的大小限制。
從更廣泛的角度來看,核心元件向記憶體安全語言過渡是一個日益增長的趨勢。雖然 PHP 的核心是以 C 語言編寫的,但其他系統正越來越多地採用像 Rust 這樣的語言,以消除整個類別的記憶體安全錯誤。然而,對於舊有的 C 語言程式庫,嚴格的靜態分析、模糊測試以及開發過程中使用 AddressSanitizer (ASan) 仍然是識別這些「量一次,讀到永遠」陷阱的最有效工具。
6. 結論
CVE-2025-14177 和
iptcembed
堆積緩衝區溢位的發現提醒我們,即使是像 PHP 這樣成熟且廣泛使用的平台,也無法免疫於基礎的記憶體安全錯誤。這些漏洞證明了 C 語言程式碼中微妙的邏輯錯誤——例如未能前進指標或信任易變的檔案大小——如何被武器化,進而危及整個網站技術棧的安全性。隨著威脅形勢的演變,重點必須持續放在強化核心直譯器以及採用現代安全實踐上,以保護龐大的 PHP 應用程式生態系統。