Opened 6 weeks ago
Last modified 6 weeks ago
#65030 accepted defect (bug)
Customize: `isLinkPreviewable()` regex overrides `customize_allowed_urls` filter for sites with `/wp-content/` in home URL
| Reported by: |
|
Owned by: |
|
|---|---|---|---|
| Milestone: | 7.1 | Priority: | normal |
| Severity: | normal | Version: | |
| Component: | Customize | Keywords: | has-patch changes-requested |
| Focuses: | javascript | Cc: |
Description
Summary
The customize_allowed_urls PHP filter allows plugins and themes to register additional previewable URLs for the Customizer. However, the hardcoded regex in isLinkPreviewable() in customize-preview.js overrides this filter for any URL whose pathname contains /wp-content/ — making the filter incomplete.
When a URL passes the settings.url.allowed check (populated by customize_allowed_urls), it should be considered previewable. Instead, the regex silently rejects it, and there is no JS-side filter or hook to override this behavior.
Steps to reproduce
- Install WordPress in a subdirectory under
wp-content/(e.g.,example.com/wp-content/subsite/) - Confirm
home_url('/')returns thewp-contentbased URL - Log in and open Appearance → Customize
- The preview iframe fails to load
Note: get_allowed_urls() includes home_url('/') by default, so the site's own URL is in the allowed list — yet the Customizer still rejects it.
Root cause
In src/js/_enqueues/wp/customize/preview.js, the isLinkPreviewable() function runs two checks in sequence:
// Check 1: URL matches an explicitly allowed URL — PASSES matchesAllowedUrl = ! _.isUndefined( _.find( api.settings.url.allowed, function( allowedUrl ) { parsedAllowedUrl.href = allowedUrl; return parsedAllowedUrl.protocol === element.protocol && parsedAllowedUrl.host === element.host && 0 === element.pathname.indexOf( parsedAllowedUrl.pathname ); } ) ); if ( ! matchesAllowedUrl ) { return false; } // Check 2: Hardcoded regex — overrides check 1 for URLs containing /wp-content/ if ( /\/wp-(admin|includes|content)(\/|$)/.test( element.pathname ) ) { return false; }
Check 2 does not consider whether the URL was already approved by check 1. A URL can be explicitly allowed via the PHP filter and still be blocked by the JS regex.
The same issue exists in the previewUrl setter in customize-controls.js.
Expected behavior
URLs that match an entry in settings.url.allowed (populated via the customize_allowed_urls PHP filter) should not be overridden by the hardcoded regex. The regex should only apply to URLs that did not match the allowed list.
Proposed fix
// Only apply the regex block for URLs that were NOT explicitly allowed if ( ! matchesAllowedUrl && /\/wp-(admin|includes|content)(\/|$)/.test( element.pathname ) ) { return false; }
This is backward-compatible. If no allowed URL contains /wp-content/, behavior is identical to today. It only changes behavior when the PHP filter has explicitly approved a URL that happens to contain /wp-content/ in its path.
Related tickets
Change History (3)
This ticket was mentioned in PR #11459 on WordPress/wordpress-develop by @abhi3315.
6 weeks ago
#1
- Keywords has-patch added
#2
@
6 weeks ago
Updated the proposed fix. The original ! matchesAllowedUrl approach wouldn't work because matchesAllowedUrl is always true at that point. The correct fix captures the matched allowed URL's path prefix and applies the regex only to the remainder. See PR: https://github.com/WordPress/wordpress-develop/pull/11459
#3
@
6 weeks ago
- Keywords changes-requested added
- Milestone changed from Awaiting Review to 7.1
- Owner set to westonruter
- Status changed from new to accepted
- Version 6.9.4 deleted
I can see this would be a problem, although it seems very uncommon.
Really, the approach was wrong to begin with in how the wp-admin, wp-includes, and wp-content paths are hard-coded. Really, these should be exported from PHP to JS, using an approach similar to what can be seen in the Speculative Loading functionality. See \WP_URL_Pattern_Prefixer::get_default_contexts(). So we should be obtaining the paths from admin_url(), plugins_url(), and content_url(), for example. This would be a more robust way to prevent navigations to URLs that aren't served by WP.
The hardcoded regex in
isLinkPreviewable()and thepreviewUrlsetter blocks any URL with/wp-content/in the path, even when the URL was explicitly allowed viacustomize_allowed_urls. This breaks the Customizer for sites installed under a/wp-content/subdirectory.Fix: capture the matched allowed URL's path prefix, apply the regex only to the remainder beyond it.
## Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 4.6
Used for: Analysis of the bug, implementation approach, and code changes. Final implementation reviewed and verified by me.