Bug Summary
The LocalWP Live Links helper appears to have a conditional data-integrity bug when saving non-string WordPress option values, such as arrays or objects, during a Live Link request. If the option contains the Live Link tunnel host, the helper may corrupt PHP serialized data because it replaces the host inside a serialized string without recalculating serialized string lengths.
Steps to reproduce
-
Create a disposable LocalWP WordPress site.
-
Enable Live Links for the site.
-
Add a small MU-plugin that, during a Live Link request, saves two options:
- A plain string option containing
home_url('/serialized-poc/') - An array option containing the same URL, including one nested array value
- A plain string option containing
-
Visit the Live Link URL while logged in as an admin, for example:
https://example.localsite.io/?llh_poc=1 -
Observe that the plain string option saves correctly, but the array option fails before the fix.
In my test before applying a fix:
- The string option saved correctly.
update_option()returnedtruefor the string option.get_option()returned the expected local URL for the string option.update_option()returnedfalsefor the array option.get_option()returnedfalsefor the array option.- The raw database value for the array option was
NULL.
After changing the helper to recursively walk arrays/objects and replace only string values, the same array option saved correctly as valid serialized PHP data.
This appears to be conditional rather than universal. Plain string options seem to pass through the string branch safely. The issue appears when a non-string option value, such as an array or object, contains the Live Link tunnel host and is saved during a Live Link request. If the host replacement changes the string length, the serialized string length markers can become invalid.
Environment Info
Describe your environment.
- Windows 11
- Current LocalWP defaults - nginx, PHP 8.2.29, MySQL 8.4.0
- Local Version 10.1.1+6939
Supporting info
The issue appears to be in LocalWP_Live_Link_Helper::make_link_local().
For non-string option values, the helper currently does this:
$old_str = serialize($option);
$new_str = $this->replace_host( $this->get_tunnel_host(), $this->home_domain, $old_str );
return unserialize($new_str);
The problem is that PHP serialized strings contain exact byte-length markers. If the Live Link tunnel host is replaced with a local host of a different length, the string contents change, but the serialized length markers do not. Then unserialize() can fail.
A safer fix direction appears to be:
- Do not run host replacement on serialized PHP text.
- Recursively walk arrays/objects.
- Replace only string values.
- Return the modified PHP value and let WordPress serialize it normally when saving the option.
Conceptually:
public function make_link_local( $option ) {
return $this->map_replace( $option, function ( $str ) {
return $this->replace_host( $this->get_tunnel_host(), $this->home_domain, $str );
} );
}
private function map_replace( $value, $callback ) {
if ( is_string( $value ) ) {
return call_user_func( $callback, $value );
}
if ( function_exists( 'map_deep' ) && ( is_array( $value ) || is_object( $value ) ) ) {
return map_deep( $value, function ( $item ) use ( $callback ) {
return is_string( $item ) ? call_user_func( $callback, $item ) : $item;
} );
}
return $value;
}
Here is the before/after output from my proof of concept.
Before fix:
Live Link Serialized Data PoC
=============================
HTTP_HOST: test-site.local
HTTP_X_ORIGINAL_HOST: screeching-quartz.localsite.io
get_option('home'): https://screeching-quartz.localsite.io
home_url('/serialized-poc/'): https://screeching-quartz.localsite.io/serialized-poc/
String option result:
update_option returned: true
get_option returned:
'https://test-site.local/serialized-poc/'
raw DB value:
'https://test-site.local/serialized-poc/'
Array option result:
update_option returned: false
get_option returned: false
raw DB value: NULL
After fix:
Live Link Serialized Data PoC
=============================
HTTP_HOST: test-site.local
HTTP_X_ORIGINAL_HOST: screeching-quartz.localsite.io
get_option('home'): https://screeching-quartz.localsite.io
home_url('/serialized-poc/'): https://screeching-quartz.localsite.io/serialized-poc/
String option result:
update_option returned: true
get_option returned:
'https://test-site.local/serialized-poc/'
raw DB value:
'https://test-site.local/serialized-poc/'
Array option result:
update_option returned: true
get_option returned:
array (
'url' => 'https://test-site.local/serialized-poc/',
'nested' =>
array (
'also_url' => 'https://test-site.local/serialized-poc/?nested=1',
),
)
raw DB value:
'a:2:{s:3:"url";s:39:"https://test-site.local/serialized-poc/";s:6:"nested";a:1:{s:8:"also_url";s:48:"https://test-site.local/serialized-poc/?nested=1";}}'
I’m not deeply familiar with LocalWP internals, so this may not be the ideal final patch. But the reproduction seems consistent: replacing the host inside serialized PHP data appears to break array option saves, while recursively replacing only string values avoids the issue.