Live Links helper can fail to save serialized option data when arrays contain tunnel URLs

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

  1. Create a disposable LocalWP WordPress site.

  2. Enable Live Links for the site.

  3. 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
  4. Visit the Live Link URL while logged in as an admin, for example:

    https://example.localsite.io/?llh_poc=1

  5. 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() returned true for the string option.
  • get_option() returned the expected local URL for the string option.
  • update_option() returned false for the array option.
  • get_option() returned false for 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:

  1. Do not run host replacement on serialized PHP text.
  2. Recursively walk arrays/objects.
  3. Replace only string values.
  4. 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.

One bit of full disclosure: I have not personally run into this issue on a real production site or with a specific third-party plugin. I was having Claude review the Live Links helper for other purposes, and it flagged this serialized-data pattern as potentially risky.

I created the small proof of concept above to verify whether the issue was real, and in my testing, it appears to reproduce consistently. So this may be a low-priority or edge-case issue, but I wanted to report it in case it helps prevent confusing Live Links-related option save failures in the future.

Thank you for sharing this, @DustinATX. We will keep this open for others to vote or comment to see if it’s an issue for any additional users. In the meantime, we are currently working on a revamped Live Links experience, so some features may function differently when it is released. We will provide more information in the Forums and on our releases page when it becomes available. Releases - Local