Make WordPress Core

Opened 4 months ago

Last modified 2 weeks ago

#65030 accepted defect (bug)

Customize: `isLinkPreviewable()` regex overrides `customize_allowed_urls` filter for sites with `/wp-content/` in home URL

Reported by: abhi3315 Owned by: westonruter
Priority: normal Milestone: 7.2
Component: Customize Version:
Severity: normal Keywords: has-patch changes-requested
Cc: Focuses: javascript

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

  1. Install WordPress in a subdirectory under wp-content/ (e.g., example.com/wp-content/subsite/)
  2. Confirm home_url('/') returns the wp-content based URL
  3. Log in and open Appearance → Customize
  4. 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

  • #38409 — Hardened URL matching in isLinkPreviewable
  • #31517 — Added non-previewable link notice in Customizer
  • #30937 — Customize changesets, introduced isLinkPreviewable

Change History (5)

This ticket was mentioned in PR #11459 on WordPress/wordpress-develop by @abhi3315.


4 months ago
#1

  • Keywords has-patch added

The hardcoded regex in isLinkPreviewable() and the previewUrl setter blocks any URL with /wp-content/ in the path, even when the URL was explicitly allowed via customize_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.

#2 @abhi3315
4 months 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 @westonruter
4 months ago

  • Keywords changes-requested added
  • Milestone Awaiting Review7.1
  • Owner set to westonruter
  • Status newaccepted
  • Version 6.9.4

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.

#4 @wildworks
2 weeks ago

  • Milestone 7.17.2

Really, these should be exported from PHP to JS, using an approach similar to what can be seen in the Speculative Loading functionality.

To move this ticket forward, we need a new or updated PR following this approach. In any case, it's too late to address this, so I'd like to punt this to 7.2.

#5 @abhi3315
2 weeks ago

Sorry, I completely lost track of it. I'll pick it up from where I left off and keep you updated on the progress.

Note: See TracTickets for help on using tickets.