Files
2meet-data-optimizer/includes/class-tmdo-safe-unserialize.php
T
wpdev 76c01e44df refactor: 全部 128 個生產檔加入 declare(strict_types=1)(PR-H)
對齊 A v3.2.0。型別強制會把隱式轉換變成 TypeError,所以一次全檔加入
並跑完整測試(unit 451 / integration 398 全綠,無迴歸)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
2026-07-31 06:13:33 +08:00

94 lines
3.1 KiB
PHP

<?php
/**
* TMDO_Safe_Unserialize — Object-injection-safe replacement for
* `maybe_unserialize()` (v2.13.3 — fixes L-DESER-1).
*
* `maybe_unserialize()` calls `unserialize()` with default options, which
* materializes objects and triggers __wakeup / __destruct magic methods.
* Attacker-planted serialized payloads in DB-stored values (postmeta,
* usermeta, options) become RCE vectors when any vendor library exposes
* a usable gadget chain.
*
* This wrapper passes `['allowed_classes' => false]` so PHP returns
* `__PHP_Incomplete_Class` placeholders without ever invoking magic
* methods on the original class. The placeholders are then walked out of
* the result tree before returning, so they cannot leak into a flat-table
* JSON column or downstream consumer.
*
* @package WP_Data_Optimizer
* @since 2.13.3
*/
declare(strict_types=1);
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Static helper class — drop-in replacement for `maybe_unserialize()`.
*/
final class TMDO_Safe_Unserialize {
/**
* Object-injection-safe `maybe_unserialize()` equivalent.
*
* Same input/output contract as `maybe_unserialize()`:
* - Non-string input is returned as-is.
* - Non-serialized strings are returned as-is.
* - Serialized arrays / scalars are unserialized with allowed_classes=false.
* - Any object placeholders in the result are stripped to null.
*
* @param mixed $value Raw value (typically meta_value or option value).
* @return mixed Unserialized array / scalar, or original string if not serialized.
*/
public static function run( $value ) {
if ( ! is_string( $value ) ) {
return $value;
}
$trimmed = trim( $value );
// Cheap inline detector — does not rely on WP's is_serialized() so this
// helper works in CLI / standalone contexts. PHP serialize tokens:
// a (array), O (object), s (string), i (int), d (float), b (bool),
// N; (null), C (custom class — also handled by allowed_classes=false).
if ( 'N;' !== $trimmed ) {
if ( strlen( $trimmed ) < 4 || ':' !== ( $trimmed[1] ?? '' ) ) {
return $value;
}
if ( ! in_array( $trimmed[0] ?? '', array( 'a', 'O', 's', 'i', 'd', 'b', 'C' ), true ) ) {
return $value;
}
}
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- allowed_classes=false hardens against object injection.
$result = @unserialize( $trimmed, array( 'allowed_classes' => false ) );
// `unserialize()` returns false on parse error. The literal payload
// `b:0;` legitimately deserializes to (bool) false, so distinguish that.
if ( false === $result && 'b:0;' !== $trimmed ) {
return $value;
}
// Strip __PHP_Incomplete_Class artefacts (allowed_classes=false replaces
// any object marker with this stub). They must never reach a flat-table
// JSON column or a downstream consumer that might try to access props.
if ( is_object( $result ) ) {
return null;
}
if ( is_array( $result ) ) {
array_walk_recursive(
$result,
static function ( &$v ) {
if ( is_object( $v ) ) {
$v = null;
}
}
);
}
return $result;
}
}