chore: initial snapshot of 2meet-data-optimizer v0.1.0
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
<?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
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user