76c01e44df
對齊 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
94 lines
3.1 KiB
PHP
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;
|
|
}
|
|
}
|