Skip to content

Commit e2e1afb

Browse files
authored
fix: Editor assets endpoint enforces absolute URLs (#45319)
* fix: Editor assets endpoint enforces absolute URLs Relative URLs inherently fail in local client editors that are served from origins other than the site origin. Using absolute URLs ensures the asset source is correct. * test: Assert editor asset endpoint enforces absolute URLs * changelog
1 parent a5b0d2e commit e2e1afb

File tree

3 files changed

+206
-0
lines changed

3 files changed

+206
-0
lines changed

projects/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-block-editor-assets.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,9 @@ public function get_items( $request ) { // phpcs:ignore VariableAnalysis.CodeAna
259259
// Unregister disallowed plugin assets before proceeding with asset collection
260260
$this->unregister_disallowed_plugin_assets();
261261

262+
add_filter( 'script_loader_src', array( $this, 'make_url_absolute' ), 10, 2 );
263+
add_filter( 'style_loader_src', array( $this, 'make_url_absolute' ), 10, 2 );
264+
262265
ob_start();
263266
wp_print_styles();
264267
$styles = ob_get_clean();
@@ -272,6 +275,9 @@ public function get_items( $request ) { // phpcs:ignore VariableAnalysis.CodeAna
272275
wp_print_footer_scripts();
273276
$scripts = ob_get_clean();
274277

278+
remove_filter( 'script_loader_src', array( $this, 'make_url_absolute' ), 10 );
279+
remove_filter( 'style_loader_src', array( $this, 'make_url_absolute' ), 10 );
280+
275281
$wp_styles = $current_wp_styles;
276282
$wp_scripts = $current_wp_scripts;
277283

@@ -367,6 +373,19 @@ private function is_allowed_plugin_handle( $handle ) {
367373
return false;
368374
}
369375

376+
/**
377+
* Convert relative URLs to absolute URLs.
378+
*
379+
* @param string $src The source URL.
380+
* @return string The absolute URL.
381+
*/
382+
public function make_url_absolute( $src ) {
383+
if ( ! empty( $src ) && strpos( $src, '/' ) === 0 && strpos( $src, '//' ) !== 0 ) {
384+
return site_url( $src );
385+
}
386+
return $src;
387+
}
388+
370389
/**
371390
* Checks the permissions for retrieving items.
372391
*
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: other
3+
4+
Editor assets endpoint: enforce absolute URLs to mitigate failed requests from client origins.

projects/plugins/jetpack/tests/php/core-api/wpcom-endpoints/WPCOM_REST_API_V2_Endpoint_Block_Editor_Assets_Test.php

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* @package Jetpack
66
*/
77

8+
use PHPUnit\Framework\Attributes\DataProvider;
89
use WpOrg\Requests\Requests;
910

1011
require_once dirname( __DIR__, 2 ) . '/lib/Jetpack_REST_TestCase.php';
@@ -450,4 +451,186 @@ public function test_preserved_scripts_maintain_properties() {
450451
public function enqueue_complex_test_script() {
451452
wp_enqueue_script( 'jetpack-complex-test-script' );
452453
}
454+
455+
/**
456+
* Test the make_url_absolute method converts URLs correctly.
457+
*
458+
* @param string $input_url The input URL.
459+
* @param string $expected_url The expected URL after conversion.
460+
* @dataProvider url_conversion_data_provider
461+
*/
462+
#[DataProvider( 'url_conversion_data_provider' )]
463+
public function test_make_url_absolute( $input_url, $expected_url ) {
464+
// Use the public method directly
465+
$result = $this->instance->make_url_absolute( $input_url );
466+
$this->assertSame( $expected_url, $result );
467+
}
468+
469+
/**
470+
* Data provider for URL conversion tests.
471+
*
472+
* @return array Test data.
473+
*/
474+
public static function url_conversion_data_provider() {
475+
$site_url = site_url();
476+
return array(
477+
'relative URL starting with /' => array( '/wp-admin/script.js', $site_url . '/wp-admin/script.js' ),
478+
'relative URL with wp-includes' => array( '/wp-includes/js/jquery.js', $site_url . '/wp-includes/js/jquery.js' ),
479+
'protocol-relative URL' => array( '//example.com/script.js', '//example.com/script.js' ),
480+
'absolute URL with http' => array( 'http://example.com/script.js', 'http://example.com/script.js' ),
481+
'absolute URL with https' => array( 'https://example.com/script.js', 'https://example.com/script.js' ),
482+
'empty string' => array( '', '' ),
483+
'relative path without leading slash' => array( 'script.js', 'script.js' ),
484+
'URL with query parameters' => array( '/wp-admin/script.js?ver=1.0', $site_url . '/wp-admin/script.js?ver=1.0' ),
485+
'URL with hash' => array( '/wp-admin/page#section', $site_url . '/wp-admin/page#section' ),
486+
);
487+
}
488+
489+
/**
490+
* Test that URL conversion filters are applied during asset generation.
491+
*/
492+
public function test_url_conversion_filters_are_applied() {
493+
wp_set_current_user( self::factory()->user->create( array( 'role' => 'editor' ) ) );
494+
495+
// Track filter application
496+
$filters_applied = array(
497+
'script' => false,
498+
'style' => false,
499+
);
500+
501+
// Add tracking filters at priority 5 (before our filter at 10)
502+
add_filter(
503+
'script_loader_src',
504+
function ( $src ) use ( &$filters_applied ) {
505+
$filters_applied['script'] = true;
506+
return $src;
507+
},
508+
5,
509+
2
510+
);
511+
512+
add_filter(
513+
'style_loader_src',
514+
function ( $src ) use ( &$filters_applied ) {
515+
$filters_applied['style'] = true;
516+
return $src;
517+
},
518+
5,
519+
2
520+
);
521+
522+
$request = new WP_REST_Request( Requests::GET, '/wpcom/v2/editor-assets' );
523+
$this->server->dispatch( $request );
524+
525+
$this->assertTrue( $filters_applied['script'], 'Script filter was not applied' );
526+
$this->assertTrue( $filters_applied['style'], 'Style filter was not applied' );
527+
528+
// Clean up
529+
remove_all_filters( 'script_loader_src', 5 );
530+
remove_all_filters( 'style_loader_src', 5 );
531+
}
532+
533+
/**
534+
* Test that relative URLs in assets are converted to absolute URLs.
535+
*/
536+
public function test_relative_urls_are_converted_to_absolute() {
537+
wp_set_current_user( self::factory()->user->create( array( 'role' => 'editor' ) ) );
538+
539+
// Register assets with relative URLs
540+
add_action(
541+
'enqueue_block_editor_assets',
542+
function () {
543+
wp_register_script( 'jetpack-relative-test', '/wp-content/plugins/test/script.js', array(), '1.0', true );
544+
wp_register_style( 'jetpack-relative-style', '/wp-content/plugins/test/style.css', array(), '1.0' );
545+
546+
wp_enqueue_script( 'jetpack-relative-test' );
547+
wp_enqueue_style( 'jetpack-relative-style' );
548+
}
549+
);
550+
551+
$request = new WP_REST_Request( Requests::GET, '/wpcom/v2/editor-assets' );
552+
$response = $this->server->dispatch( $request );
553+
$data = $response->get_data();
554+
555+
// Check that relative URLs have been converted to absolute
556+
$site_url = site_url();
557+
$this->assertStringContainsString( $site_url . '/wp-content/plugins/test/script.js', $data['scripts'] );
558+
$this->assertStringContainsString( $site_url . '/wp-content/plugins/test/style.css', $data['styles'] );
559+
560+
// Ensure no relative URLs remain in src/href attributes
561+
$this->assertDoesNotMatchRegularExpression( '/src=[\'"]\//', $data['scripts'], 'Found relative URL in script src' );
562+
$this->assertDoesNotMatchRegularExpression( '/href=[\'"]\//', $data['styles'], 'Found relative URL in style href' );
563+
}
564+
565+
/**
566+
* Test that core WordPress assets with relative URLs are converted to absolute.
567+
*/
568+
public function test_core_assets_urls_are_absolute() {
569+
wp_set_current_user( self::factory()->user->create( array( 'role' => 'editor' ) ) );
570+
571+
$request = new WP_REST_Request( Requests::GET, '/wpcom/v2/editor-assets' );
572+
$response = $this->server->dispatch( $request );
573+
$data = $response->get_data();
574+
575+
$site_url = site_url();
576+
577+
// Check that any wp-includes or wp-admin paths are absolute (either local or CDN)
578+
if ( strpos( $data['scripts'], 'wp-includes' ) !== false || strpos( $data['scripts'], 'wp-admin' ) !== false ) {
579+
// Verify URLs are absolute (not relative)
580+
preg_match_all( '/(?:src|href)=[\'"]([^\'"]+)[\'"]/', $data['scripts'], $script_urls );
581+
foreach ( $script_urls[1] as $url ) {
582+
if ( strpos( $url, 'wp-includes' ) !== false || strpos( $url, 'wp-admin' ) !== false ) {
583+
// URL should be absolute - either starts with site URL, http://, https://, or // (protocol-relative)
584+
$is_absolute = (
585+
strpos( $url, $site_url ) === 0 ||
586+
strpos( $url, 'http://' ) === 0 ||
587+
strpos( $url, 'https://' ) === 0 ||
588+
strpos( $url, '//' ) === 0
589+
);
590+
$this->assertTrue( $is_absolute, "Core script URL should be absolute, got: {$url}" );
591+
592+
// Ensure it's not a relative URL (starting with / but not //)
593+
if ( strpos( $url, '/' ) === 0 ) {
594+
$this->assertStringStartsWith( '//', $url, "URL starting with / should be protocol-relative (//), got: {$url}" );
595+
}
596+
}
597+
}
598+
}
599+
600+
if ( strpos( $data['styles'], 'wp-includes' ) !== false || strpos( $data['styles'], 'wp-admin' ) !== false ) {
601+
// Verify URLs are absolute (not relative)
602+
preg_match_all( '/(?:src|href)=[\'"]([^\'"]+)[\'"]/', $data['styles'], $style_urls );
603+
foreach ( $style_urls[1] as $url ) {
604+
if ( strpos( $url, 'wp-includes' ) !== false || strpos( $url, 'wp-admin' ) !== false ) {
605+
// URL should be absolute - either starts with site URL, http://, https://, or // (protocol-relative)
606+
$is_absolute = (
607+
strpos( $url, $site_url ) === 0 ||
608+
strpos( $url, 'http://' ) === 0 ||
609+
strpos( $url, 'https://' ) === 0 ||
610+
strpos( $url, '//' ) === 0
611+
);
612+
$this->assertTrue( $is_absolute, "Core style URL should be absolute, got: {$url}" );
613+
614+
// Ensure it's not a relative URL (starting with / but not //)
615+
if ( strpos( $url, '/' ) === 0 ) {
616+
$this->assertStringStartsWith( '//', $url, "URL starting with / should be protocol-relative (//), got: {$url}" );
617+
}
618+
}
619+
}
620+
}
621+
}
622+
623+
/**
624+
* Test that URL conversion filters are removed after processing.
625+
*/
626+
public function test_url_conversion_filters_are_removed_after_processing() {
627+
wp_set_current_user( self::factory()->user->create( array( 'role' => 'editor' ) ) );
628+
629+
$request = new WP_REST_Request( Requests::GET, '/wpcom/v2/editor-assets' );
630+
$this->server->dispatch( $request );
631+
632+
// Check that our filters are not present after the request
633+
$this->assertFalse( has_filter( 'script_loader_src', array( $this->instance, 'make_url_absolute' ) ), 'Script filter should be removed after processing' );
634+
$this->assertFalse( has_filter( 'style_loader_src', array( $this->instance, 'make_url_absolute' ) ), 'Style filter should be removed after processing' );
635+
}
453636
}

0 commit comments

Comments
 (0)