Security (low): request host/scheme persisted into block navigation and post guid, replayed on every host #10

Closed
opened 2026-10-15 22:36:02 -07:00 by eric · 0 comments
Owner

Severity: low (likelihood low, impact low)
Found by: security audit run 3 (commit 01195c8), independently verified in a disposable local WordPress 7.1.3 lab
Fingerprint: multi-domain-redux:HookBindings.register:block-render-output-not-rewritten-persisted-request-host

Summary

The plugin rewrites home_url (and the other URL filters) to the current request's host and scheme in every context, including core code paths that persist data, and the persisted values are later printed by emitters the plugin does not rewrite on output, so one request's host reaches visitors on every host. Sink (a), unauthenticated: on a block theme whose navigation block has no ref, with a classic menu and no published wp_navigation post (typical right after switching from a classic theme), the first front-end render runs core's navigation fallback, which converts the classic menu into a new wp_navigation post (post_author 0) whose link URLs are built with get_permalink() -> home_url(), already rewritten by the plugin to the requester's host and scheme. The navigation block renders that stored post_content through no plugin content filter, so every later visitor on every configured host gets those URLs in the site-wide header navigation. On a single-site lab, one anonymous request with Host set to the onion host made the clearnet www and apex homepages emit href="http://<onion>/sample-page/" in their header navigation, disclosing the onion address to every clearnet visitor and giving clearnet visitors an unusable link. The attacker chooses which configured host (and the request scheme) is stored, not an arbitrary domain, because UrlRewriter only substitutes configured hosts. The output gap also exists on its own: a canonical navigation post stored by core without the plugin is emitted with the primary host on the onion and apex hosts. Sink (b), authenticated: when an author/editor/admin opens post-new.php while on a non-primary configured host such as the onion, core stores the auto-draft guid as get_permalink() = http://<onion>/?p=N. The guid is permanent and not filtered by the plugin on output, so clearnet RSS readers and REST clients receive <guid>http://<onion>/?p=N</guid> and guid.rendered next to a correctly rewritten clearnet link, disclosing the onion address and its correlation to the clearnet site even when the operator publishes no onion hreflang.

Root cause

UrlRewriter::rewrite (src/Rewrite/UrlRewriter.php:61) maps any configured-host absolute URL to the current request's scheme and host, and is registered on home_url and the other URL filters (src/Rewrite/HookBindings.php:56-57) with no distinction for core code paths that persist data (the wp_navigation fallback built during a front-end render, and the guid set by wp_insert_post for a post-new.php auto-draft). On output, HookBindings::register attaches ContentRewriter only to the_content, widget_text_content and comment_text (src/Rewrite/HookBindings.php:64-66); block-rendered output (render_block) and get_the_guid are never rewritten, so a stored request-specific URL is printed verbatim on other hosts.

Expected behaviour

Every URL the site emits uses the visitor's own host and never exposes another alias (such as the onion host) to visitors on a different host, and data persisted as a side effect of one request (an anonymous visitor's or an author's) must not fix the host or scheme other visitors see.

Source trace

  1. src/Request/HostResolver.php:58 (HostResolver::resolve): Reads the client-supplied Host header from the $_SERVER copy made at line 56; the request scheme is read at line 59. For sink (a) this is an unauthenticated front-end request; for sink (b) the author's admin request.
  2. src/Request/HostResolver.php:61 (HostResolver::resolve): Matches the Host against configured hosts with primary fallback (lines 61-64); onion forces http (67-69); the chosen host and scheme are memoized at lines 71-72.
  3. src/Rewrite/HookBindings.php:57 (HookBindings::register): rewriteUrlFilter is registered on home_url and the other URL filters with no distinction for data-persisting contexts.
  4. src/Rewrite/UrlRewriter.php:61 (UrlRewriter::rewrite): The permalink core computes becomes <request scheme>://<request host>/path, which core stores in the fallback wp_navigation post_content (url attribute) and, for a post-new.php auto-draft, in posts.guid.
  5. src/Rewrite/HookBindings.php:64 (HookBindings::register): Only the_content, widget_text_content and comment_text are rewritten on output (lines 64-66); navigation-block render output and get_the_guid (RSS <guid>, REST guid.rendered) are not, so the stored host and scheme are emitted on every host.

Conditions

  • authentication level: Sink (a): no authentication; the first visitor whose request triggers the navigation fallback decides the stored host and scheme.
  • system configuration: Sink (a): block theme whose navigation block has no ref, at least one classic menu with items, and no published wp_navigation post; typical right after switching from a classic to a block theme. A menu-less fallback stores a bare <!-- wp:page-list /--> with no URLs and is not affected.
  • data state: Sink (a): only the single render that creates the fallback post; later renders reuse the stored post until an admin edits or replaces it.
  • system configuration: Two or more hosts configured; the stored host can only be a configured host or the primary, so the attacker picks which configured host and scheme, not an arbitrary domain. Disclosure of the onion address via sink (a) requires the attacker to already know it.
  • authorization role: Sink (b): a user who can create posts opens post-new.php while browsing on a non-primary configured host such as the onion; passive, not attacker-driven, and relevant when the operator has not already published that host (e.g. via hreflang).
  • third party dependency: Relies on core behaviour observed directly on WordPress 7.1.3 in the lab: WP_Navigation_Fallback converts a classic menu into a wp_navigation post during an unauthenticated front-end render; the navigation block renders stored url attributes without the_content; wp_insert_post sets the auto-draft guid from get_permalink() and keeps it on publish.

Lab reproduction (local, dummy data only)

Principal: Sink (a): unauthenticated network client controlling the Host header (and scheme) of one front-end request. Sink (b): no attacker request; a legitimate author working on the onion host causes it and any clearnet feed or REST reader observes it.

  1. Single-site WordPress 7.1.3 with Twenty Twenty-Five; plugin active with hosts www.example.test (primary), example.test and <onion>, all with empty lang; delete any wp_navigation post and create a classic menu containing page 2 (Sample Page).
  2. Send one unauthenticated plain-http GET /?cb=<unique> with Host: <onion>.
  3. wp post list --post_type=wp_navigation and wp post get <id>: check post_author, guid and the url attribute in post_content.
  4. GET /?cb=<unique> with Host: www.example.test and with Host: example.test; extract the wp-block-navigation-item__content hrefs and the feed link.
  5. Control: delete the navigation post, deactivate the plugin, render once with Host: www.example.test and inspect the stored url; reactivate and render on the onion and example.test hosts.
  6. guid: log in as the dummy admin over Host: <onion>, GET /wp-admin/post-new.php?cb=<unique>, publish that auto-draft; unauthenticated, fetch /feed/?cb=<unique> and /?rest_route=/wp/v2/posts/<id> with Host: www.example.test and compare <guid>/guid.rendered with <link>/link. Control: repeat post-new.php with Host: www.example.test.
  7. Restore: delete created posts, navigation posts and menu, deactivate the plugin, delete the settings option and destroy the admin sessions. Use cache-busting query strings because nginx fastcgi_cache otherwise serves stale pages.

Observed: Verifier v9 reproduced both sinks on single-site lab 3. (a) One anonymous GET with the onion Host created wp_navigation post 11 with post_author 0, guid http://<onion>/2026/10/16/v9menu/ and url "http://<onion>/sample-page/"; the www.example.test and example.test homepages then both emitted href="http://<onion>/sample-page/" in the header navigation while their feed links were correctly http://www.example.test/feed/ and http://example.test/feed/. Control: without the plugin core stored http://www.example.test/sample-page/, and with the plugin reactivated that URL was emitted unrewritten on the onion and example.test hosts. (b) An admin's post-new.php on the onion host produced auto-draft 18 with guid http://<onion>/?p=18, unchanged after publish; the www feed emitted <link>http://www.example.test/2026/10/16/v9-guid-probe/</link> with <guid isPermaLink="false">http://<onion>/?p=18</guid>, and REST guid.rendered was the onion URL while link was www; the www homepage contained no onion reference. Control on the www host: guid http://www.example.test/?p=21. Consistent with the earlier multisite reproductions by hunter w4b and verifier v7 and hunter w5a's guid run. The lab was restored. The scheme dimension (a stored plain-http URL served to https visitors) was not re-observed because the lab is http-only.

Fix

Rewrite block-rendered output at emit time like post content: hook render_block and widget_block_content to a string-guarded wrapper. Because ContentRewriter skips the visitor's own host (src/Rewrite/ContentRewriter.php:38-40), also drop that skip and normalise a stored scheme on the current host using a never-downgrade rule (keep https for clearnet hosts when the stored URL was https; onion stays http); same-host rewriting is idempotent when the scheme already matches. For guids, do not make the identifier depend on the visitor's host (feed readers treat it as a stable ID): pin guids to the primary host with its configured scheme on write (wp_insert_post_data) and, on output, map any configured-host guid to the primary (get_the_guid), which also repairs guids already stored. Optionally avoid persisting request-specific URLs during wp_navigation fallback creation, but the emit-time rewrite is the control that enforces the invariant. Regression tests: (1) a stored navigation-link url on host A rendered on host B emits host B; (2) a stored http:// url on clearnet host B rendered on B over https emits https; (3) a stored https:// url on B rendered on B over http stays https; (4) a post created during an onion-host admin request has a primary-host guid and the www feed and REST output contain no onion host.

Suggested change in src/Rewrite/HookBindings.php:

		// Inline content.
		add_filter( 'the_content', array( $this->content, 'rewrite' ), 99 );
		add_filter( 'widget_text_content', array( $this->content, 'rewrite' ), 99 );
		add_filter( 'widget_block_content', array( $this, 'rewriteBlockOutput' ), 99, 1 );
		add_filter( 'render_block', array( $this, 'rewriteBlockOutput' ), 99, 1 );
		add_filter( 'comment_text', array( $this->content, 'rewrite' ), 99 );

		// Stable identifiers: never expose an alias host through guids.
		add_filter( 'get_the_guid', array( $this, 'pinToPrimary' ), 99, 1 );
		add_filter( 'wp_insert_post_data', array( $this, 'pinInsertGuid' ), 99, 1 );

	// ... (rest of register() unchanged) ...

	/**
	 * @param mixed $html Rendered block markup.
	 */
	public function rewriteBlockOutput( $html ): mixed {
		return is_string( $html ) ? $this->content->rewrite( $html ) : $html;
	}

	/**
	 * Maps a configured-host URL to the primary host using the stored home scheme.
	 *
	 * @param mixed $url
	 */
	public function pinToPrimary( $url ): mixed {
		$primary = $this->settings->primary();
		if ( ! is_string( $url ) || null === $primary ) {
			return $url;
		}
		$parts = wp_parse_url( $url );
		if ( ! is_array( $parts ) || empty( $parts['host'] ) || null === $this->settings->findByHost( strtolower( (string) $parts['host'] ) ) ) {
			return $url;
		}
		$scheme = (string) ( wp_parse_url( (string) get_option( 'home' ), PHP_URL_SCHEME ) ?: 'https' );
		$tail   = (string) ( $parts['path'] ?? '' ) . ( isset( $parts['query'] ) ? '?' . $parts['query'] : '' ) . ( isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : '' );
		return $scheme . '://' . $primary->host . $tail;
	}

	/**
	 * @param mixed $data wp_insert_post_data array
	 */
	public function pinInsertGuid( $data ): mixed {
		if ( is_array( $data ) && isset( $data['guid'] ) && is_string( $data['guid'] ) && '' !== $data['guid'] ) {
			$data['guid'] = (string) $this->pinToPrimary( $data['guid'] );
		}
		return $data;
	}

Suggested change in src/Rewrite/ContentRewriter.php:

		foreach ( $this->settings->hosts as $host ) {
			// Include the visitor's own host so a stored scheme/host persisted by another
			// request is normalised. Never downgrade: keep https for clearnet targets when
			// the matched URL was https.
			$quoted  = preg_quote( $host->host, '~' );
			$pattern = '~(?P<scheme>https?:)?//' . $quoted . '(?::\d{1,5})?(?P<tail>(?=[/"\'\s<>?#)]|$))~i';
			$content = (string) preg_replace_callback(
				$pattern,
				static function ( array $m ) use ( $targetScheme, $targetHost, $current ): string {
					$scheme = $targetScheme;
					if ( ! $current->isOnion() && 'https:' === strtolower( (string) ( $m['scheme'] ?? '' ) ) ) {
						$scheme = 'https';
					}
					return $scheme . '://' . $targetHost;
				},
				$content
			);
		}

Priority rationale

  • Likelihood: Sink (a) needs the narrow one-time window where a block theme has a classic menu and no published wp_navigation post (no authentication; first request wins). Sink (b) happens passively whenever an author creates a post while on the onion host, but matters only to operators who keep the onion/clearnet correlation unpublished.
  • Impact: Integrity and disclosure with limited blast radius: site-wide header navigation on every host points to a configured host (and possibly scheme) chosen by one requester, and post guids in clearnet feeds/REST reveal the onion address and its correlation to the clearnet site. No arbitrary domain, no secret beyond the configured alias, and the navigation is admin-repairable.
**Severity:** low (likelihood low, impact low) **Found by:** security audit run 3 (commit `01195c8`), independently verified in a disposable local WordPress 7.1.3 lab **Fingerprint:** `multi-domain-redux:HookBindings.register:block-render-output-not-rewritten-persisted-request-host` ## Summary The plugin rewrites home_url (and the other URL filters) to the current request's host and scheme in every context, including core code paths that persist data, and the persisted values are later printed by emitters the plugin does not rewrite on output, so one request's host reaches visitors on every host. Sink (a), unauthenticated: on a block theme whose navigation block has no ref, with a classic menu and no published wp_navigation post (typical right after switching from a classic theme), the first front-end render runs core's navigation fallback, which converts the classic menu into a new wp_navigation post (post_author 0) whose link URLs are built with get_permalink() -&gt; home_url(), already rewritten by the plugin to the requester's host and scheme. The navigation block renders that stored post_content through no plugin content filter, so every later visitor on every configured host gets those URLs in the site-wide header navigation. On a single-site lab, one anonymous request with Host set to the onion host made the clearnet www and apex homepages emit href="http://&lt;onion&gt;/sample-page/" in their header navigation, disclosing the onion address to every clearnet visitor and giving clearnet visitors an unusable link. The attacker chooses which configured host (and the request scheme) is stored, not an arbitrary domain, because UrlRewriter only substitutes configured hosts. The output gap also exists on its own: a canonical navigation post stored by core without the plugin is emitted with the primary host on the onion and apex hosts. Sink (b), authenticated: when an author/editor/admin opens post-new.php while on a non-primary configured host such as the onion, core stores the auto-draft guid as get_permalink() = http://&lt;onion&gt;/?p=N. The guid is permanent and not filtered by the plugin on output, so clearnet RSS readers and REST clients receive &lt;guid&gt;http://&lt;onion&gt;/?p=N&lt;/guid&gt; and guid.rendered next to a correctly rewritten clearnet link, disclosing the onion address and its correlation to the clearnet site even when the operator publishes no onion hreflang. ## Root cause UrlRewriter::rewrite (src/Rewrite/UrlRewriter.php:61) maps any configured-host absolute URL to the current request's scheme and host, and is registered on home_url and the other URL filters (src/Rewrite/HookBindings.php:56-57) with no distinction for core code paths that persist data (the wp_navigation fallback built during a front-end render, and the guid set by wp_insert_post for a post-new.php auto-draft). On output, HookBindings::register attaches ContentRewriter only to the_content, widget_text_content and comment_text (src/Rewrite/HookBindings.php:64-66); block-rendered output (render_block) and get_the_guid are never rewritten, so a stored request-specific URL is printed verbatim on other hosts. ## Expected behaviour Every URL the site emits uses the visitor's own host and never exposes another alias (such as the onion host) to visitors on a different host, and data persisted as a side effect of one request (an anonymous visitor's or an author's) must not fix the host or scheme other visitors see. ## Source trace 1. `src/Request/HostResolver.php:58` (HostResolver::resolve): Reads the client-supplied Host header from the $_SERVER copy made at line 56; the request scheme is read at line 59. For sink (a) this is an unauthenticated front-end request; for sink (b) the author's admin request. 2. `src/Request/HostResolver.php:61` (HostResolver::resolve): Matches the Host against configured hosts with primary fallback (lines 61-64); onion forces http (67-69); the chosen host and scheme are memoized at lines 71-72. 3. `src/Rewrite/HookBindings.php:57` (HookBindings::register): rewriteUrlFilter is registered on home_url and the other URL filters with no distinction for data-persisting contexts. 4. `src/Rewrite/UrlRewriter.php:61` (UrlRewriter::rewrite): The permalink core computes becomes &lt;request scheme&gt;://&lt;request host&gt;/path, which core stores in the fallback wp_navigation post_content (url attribute) and, for a post-new.php auto-draft, in posts.guid. 5. `src/Rewrite/HookBindings.php:64` (HookBindings::register): Only the_content, widget_text_content and comment_text are rewritten on output (lines 64-66); navigation-block render output and get_the_guid (RSS &lt;guid&gt;, REST guid.rendered) are not, so the stored host and scheme are emitted on every host. ## Conditions - **authentication level**: Sink (a): no authentication; the first visitor whose request triggers the navigation fallback decides the stored host and scheme. - **system configuration**: Sink (a): block theme whose navigation block has no ref, at least one classic menu with items, and no published wp_navigation post; typical right after switching from a classic to a block theme. A menu-less fallback stores a bare &lt;!-- wp:page-list /--&gt; with no URLs and is not affected. - **data state**: Sink (a): only the single render that creates the fallback post; later renders reuse the stored post until an admin edits or replaces it. - **system configuration**: Two or more hosts configured; the stored host can only be a configured host or the primary, so the attacker picks which configured host and scheme, not an arbitrary domain. Disclosure of the onion address via sink (a) requires the attacker to already know it. - **authorization role**: Sink (b): a user who can create posts opens post-new.php while browsing on a non-primary configured host such as the onion; passive, not attacker-driven, and relevant when the operator has not already published that host (e.g. via hreflang). - **third party dependency**: Relies on core behaviour observed directly on WordPress 7.1.3 in the lab: WP_Navigation_Fallback converts a classic menu into a wp_navigation post during an unauthenticated front-end render; the navigation block renders stored url attributes without the_content; wp_insert_post sets the auto-draft guid from get_permalink() and keeps it on publish. ## Lab reproduction (local, dummy data only) **Principal:** Sink (a): unauthenticated network client controlling the Host header (and scheme) of one front-end request. Sink (b): no attacker request; a legitimate author working on the onion host causes it and any clearnet feed or REST reader observes it. 1. Single-site WordPress 7.1.3 with Twenty Twenty-Five; plugin active with hosts www.example.test (primary), example.test and &lt;onion&gt;, all with empty lang; delete any wp_navigation post and create a classic menu containing page 2 (Sample Page). 2. Send one unauthenticated plain-http GET /?cb=&lt;unique&gt; with Host: &lt;onion&gt;. 3. wp post list --post_type=wp_navigation and wp post get &lt;id&gt;: check post_author, guid and the url attribute in post_content. 4. GET /?cb=&lt;unique&gt; with Host: www.example.test and with Host: example.test; extract the wp-block-navigation-item__content hrefs and the feed link. 5. Control: delete the navigation post, deactivate the plugin, render once with Host: www.example.test and inspect the stored url; reactivate and render on the onion and example.test hosts. 6. guid: log in as the dummy admin over Host: &lt;onion&gt;, GET /wp-admin/post-new.php?cb=&lt;unique&gt;, publish that auto-draft; unauthenticated, fetch /feed/?cb=&lt;unique&gt; and /?rest_route=/wp/v2/posts/&lt;id&gt; with Host: www.example.test and compare &lt;guid&gt;/guid.rendered with &lt;link&gt;/link. Control: repeat post-new.php with Host: www.example.test. 7. Restore: delete created posts, navigation posts and menu, deactivate the plugin, delete the settings option and destroy the admin sessions. Use cache-busting query strings because nginx fastcgi_cache otherwise serves stale pages. **Observed:** Verifier v9 reproduced both sinks on single-site lab 3. (a) One anonymous GET with the onion Host created wp_navigation post 11 with post_author 0, guid http://&lt;onion&gt;/2026/10/16/v9menu/ and url "http://&lt;onion&gt;/sample-page/"; the www.example.test and example.test homepages then both emitted href="http://&lt;onion&gt;/sample-page/" in the header navigation while their feed links were correctly http://www.example.test/feed/ and http://example.test/feed/. Control: without the plugin core stored http://www.example.test/sample-page/, and with the plugin reactivated that URL was emitted unrewritten on the onion and example.test hosts. (b) An admin's post-new.php on the onion host produced auto-draft 18 with guid http://&lt;onion&gt;/?p=18, unchanged after publish; the www feed emitted &lt;link&gt;http://www.example.test/2026/10/16/v9-guid-probe/&lt;/link&gt; with &lt;guid isPermaLink="false"&gt;http://&lt;onion&gt;/?p=18&lt;/guid&gt;, and REST guid.rendered was the onion URL while link was www; the www homepage contained no onion reference. Control on the www host: guid http://www.example.test/?p=21. Consistent with the earlier multisite reproductions by hunter w4b and verifier v7 and hunter w5a's guid run. The lab was restored. The scheme dimension (a stored plain-http URL served to https visitors) was not re-observed because the lab is http-only. ## Fix Rewrite block-rendered output at emit time like post content: hook render_block and widget_block_content to a string-guarded wrapper. Because ContentRewriter skips the visitor's own host (src/Rewrite/ContentRewriter.php:38-40), also drop that skip and normalise a stored scheme on the current host using a never-downgrade rule (keep https for clearnet hosts when the stored URL was https; onion stays http); same-host rewriting is idempotent when the scheme already matches. For guids, do not make the identifier depend on the visitor's host (feed readers treat it as a stable ID): pin guids to the primary host with its configured scheme on write (wp_insert_post_data) and, on output, map any configured-host guid to the primary (get_the_guid), which also repairs guids already stored. Optionally avoid persisting request-specific URLs during wp_navigation fallback creation, but the emit-time rewrite is the control that enforces the invariant. Regression tests: (1) a stored navigation-link url on host A rendered on host B emits host B; (2) a stored http:// url on clearnet host B rendered on B over https emits https; (3) a stored https:// url on B rendered on B over http stays https; (4) a post created during an onion-host admin request has a primary-host guid and the www feed and REST output contain no onion host. Suggested change in `src/Rewrite/HookBindings.php`: ```php // Inline content. add_filter( 'the_content', array( $this->content, 'rewrite' ), 99 ); add_filter( 'widget_text_content', array( $this->content, 'rewrite' ), 99 ); add_filter( 'widget_block_content', array( $this, 'rewriteBlockOutput' ), 99, 1 ); add_filter( 'render_block', array( $this, 'rewriteBlockOutput' ), 99, 1 ); add_filter( 'comment_text', array( $this->content, 'rewrite' ), 99 ); // Stable identifiers: never expose an alias host through guids. add_filter( 'get_the_guid', array( $this, 'pinToPrimary' ), 99, 1 ); add_filter( 'wp_insert_post_data', array( $this, 'pinInsertGuid' ), 99, 1 ); // ... (rest of register() unchanged) ... /** * @param mixed $html Rendered block markup. */ public function rewriteBlockOutput( $html ): mixed { return is_string( $html ) ? $this->content->rewrite( $html ) : $html; } /** * Maps a configured-host URL to the primary host using the stored home scheme. * * @param mixed $url */ public function pinToPrimary( $url ): mixed { $primary = $this->settings->primary(); if ( ! is_string( $url ) || null === $primary ) { return $url; } $parts = wp_parse_url( $url ); if ( ! is_array( $parts ) || empty( $parts['host'] ) || null === $this->settings->findByHost( strtolower( (string) $parts['host'] ) ) ) { return $url; } $scheme = (string) ( wp_parse_url( (string) get_option( 'home' ), PHP_URL_SCHEME ) ?: 'https' ); $tail = (string) ( $parts['path'] ?? '' ) . ( isset( $parts['query'] ) ? '?' . $parts['query'] : '' ) . ( isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : '' ); return $scheme . '://' . $primary->host . $tail; } /** * @param mixed $data wp_insert_post_data array */ public function pinInsertGuid( $data ): mixed { if ( is_array( $data ) && isset( $data['guid'] ) && is_string( $data['guid'] ) && '' !== $data['guid'] ) { $data['guid'] = (string) $this->pinToPrimary( $data['guid'] ); } return $data; } ``` Suggested change in `src/Rewrite/ContentRewriter.php`: ```php foreach ( $this->settings->hosts as $host ) { // Include the visitor's own host so a stored scheme/host persisted by another // request is normalised. Never downgrade: keep https for clearnet targets when // the matched URL was https. $quoted = preg_quote( $host->host, '~' ); $pattern = '~(?P<scheme>https?:)?//' . $quoted . '(?::\d{1,5})?(?P<tail>(?=[/"\'\s<>?#)]|$))~i'; $content = (string) preg_replace_callback( $pattern, static function ( array $m ) use ( $targetScheme, $targetHost, $current ): string { $scheme = $targetScheme; if ( ! $current->isOnion() && 'https:' === strtolower( (string) ( $m['scheme'] ?? '' ) ) ) { $scheme = 'https'; } return $scheme . '://' . $targetHost; }, $content ); } ``` ## Priority rationale - Likelihood: Sink (a) needs the narrow one-time window where a block theme has a classic menu and no published wp_navigation post (no authentication; first request wins). Sink (b) happens passively whenever an author creates a post while on the onion host, but matters only to operators who keep the onion/clearnet correlation unpublished. - Impact: Integrity and disclosure with limited blast radius: site-wide header navigation on every host points to a configured host (and possibly scheme) chosen by one requester, and post guids in clearnet feeds/REST reveal the onion address and its correlation to the clearnet site. No arbitrary domain, no secret beyond the configured alias, and the navigation is admin-repairable.
eric closed this issue 2026-10-15 23:41:33 -07:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
eric/multi-domain-redux#10
No description provided.