Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions includes/create-theme/theme-media.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ public static function is_allowed_media_url( $url ) {
'jpeg',
'png',
'gif',
'svg',
'webp',
'avif',
// videos
Expand Down Expand Up @@ -147,7 +146,6 @@ public static function is_allowed_media_url( $url ) {
* - webp → `RIFF....WEBP`
* - avif → `ftyp` at offset 4 + brand `avif` or `avis` as the major
* brand OR anywhere in the compatible-brands list (ISO BMFF)
* - svg → `<svg` somewhere in the first 1024 bytes
* - mp4 / m4v / mov / 3gp / 3g2 → `ftyp` at offset 4 (ISO BMFF)
* - webm → `\x1a\x45\xdf\xa3` (EBML)
* - ogv → `OggS`
Expand All @@ -170,9 +168,7 @@ public static function is_allowed_media_file( $tmp_file, $url ) {
$path = (string) wp_parse_url( $url, PHP_URL_PATH );
$extension = strtolower( pathinfo( $path, PATHINFO_EXTENSION ) );

// Read 1024 bytes — covers all fixed-position magic-byte formats and
// gives enough room for SVG's `<svg` tag after an optional XML
// declaration / BOM / whitespace.
// Read 1024 bytes — covers all fixed-position magic-byte formats.
$fp = fopen( $tmp_file, 'rb' );
if ( false === $fp ) {
return false;
Expand Down Expand Up @@ -231,7 +227,9 @@ public static function is_allowed_media_file( $tmp_file, $url ) {
return (bool) array_intersect( $brands, array( 'avif', 'avis' ) );

case 'svg':
return false !== stripos( $head, '<svg' );
// SVGs can still be referenced as theme-owned assets, but
// media localization does not download and copy them from URLs.
return false;

case 'mp4':
case 'm4v':
Expand Down
69 changes: 65 additions & 4 deletions tests/test-theme-media.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ public function test_is_allowed_media_url_ignores_query_string() {
$this->assertFalse( CBT_Theme_Media::is_allowed_media_url( 'http://example.com/evil.php?disguised=cat.jpg' ) );
}

public function test_is_allowed_media_url_rejects_svg_extension() {
$this->assertFalse( CBT_Theme_Media::is_allowed_media_url( 'http://example.com/logo.svg' ) );
$this->assertFalse( CBT_Theme_Media::is_allowed_media_url( 'http://example.com/LOGO.SVG' ) );
}

public function test_is_allowed_media_file_accepts_real_png() {
$tmp = wp_tempnam( 'cbt-test-png' );
copy( __DIR__ . '/data/tiny.png', $tmp );
Expand Down Expand Up @@ -151,6 +156,17 @@ private function assert_media_magic_accepted( $bytes, $url ) {
$this->assertTrue( $ok, "Should accept magic bytes for $url" );
}

/**
* Helper: write magic bytes to a tmp file and assert rejection.
*/
private function assert_media_magic_rejected( $bytes, $url ) {
$tmp = wp_tempnam( 'cbt-test-magic' );
file_put_contents( $tmp, $bytes );
$ok = CBT_Theme_Media::is_allowed_media_file( $tmp, $url );
@unlink( $tmp );
$this->assertFalse( $ok, "Should reject magic bytes for $url" );
}

public function test_is_allowed_media_file_accepts_jpeg_magic() {
$this->assert_media_magic_accepted( "\xff\xd8\xff\xe0" . str_repeat( "\x00", 16 ), 'http://example.com/cat.jpg' );
}
Expand Down Expand Up @@ -202,15 +218,15 @@ public function test_is_allowed_media_file_rejects_heic_disguised_as_avif() {
$this->assertFalse( $ok, 'HEIC (no avif/avis brand) must be rejected when URL claims .avif' );
}

public function test_is_allowed_media_file_accepts_svg_content() {
$this->assert_media_magic_accepted( '<svg xmlns="http://www.w3.org/2000/svg"><circle cx="5" cy="5" r="3"/></svg>', 'http://example.com/cat.svg' );
public function test_is_allowed_media_file_rejects_svg_content() {
$this->assert_media_magic_rejected( '<svg xmlns="http://www.w3.org/2000/svg"><circle cx="5" cy="5" r="3"/></svg>', 'http://example.com/cat.svg' );
}

public function test_is_allowed_media_file_accepts_svg_with_xml_declaration() {
public function test_is_allowed_media_file_rejects_svg_with_xml_declaration() {
// Use a `?` ` >` split for the closing tag so PHP does not exit the
// file's PHP mode inside this string literal.
$payload = '<' . '?xml version="1.0" encoding="UTF-8"?' . '>' . '<svg xmlns="http://www.w3.org/2000/svg"/>';
$this->assert_media_magic_accepted( $payload, 'http://example.com/cat.svg' );
$this->assert_media_magic_rejected( $payload, 'http://example.com/cat.svg' );
}

public function test_is_allowed_media_file_accepts_mp4_ftyp() {
Expand Down Expand Up @@ -262,6 +278,13 @@ public function test_make_relative_media_url_uses_path_filename_not_query_basena
$this->assertStringNotContainsString( 'evil.php', $relative_url );
}

public function test_make_relative_media_url_keeps_svg_in_images_folder() {
$relative_url = CBT_Theme_Media::make_relative_media_url( 'http://example.com/logo.svg' );

$this->assertStringContainsString( 'get_template_directory_uri', $relative_url );
$this->assertStringContainsString( '/assets/images/logo.svg', $relative_url );
}

public function test_make_template_images_local_does_not_rewrite_disallowed_url() {
$template = new stdClass();
$template->content = '
Expand All @@ -276,6 +299,20 @@ public function test_make_template_images_local_does_not_rewrite_disallowed_url(
$this->assertStringNotContainsString( '/assets/', $new_template->content );
}

public function test_make_template_images_local_does_not_rewrite_svg_url() {
$template = new stdClass();
$template->content = '
<!-- wp:image -->
<figure class="wp-block-image"><img src="http://example.com/logo.svg" alt="" /></figure>
<!-- /wp:image -->
';

$new_template = CBT_Theme_Media::make_template_images_local( $template );

$this->assertStringContainsString( 'http://example.com/logo.svg', $new_template->content );
$this->assertStringNotContainsString( '/assets/images/logo.svg', $new_template->content );
}

public function test_make_template_images_local_only_rewrites_validated_media() {
$template = new stdClass();
$template->content = '
Expand Down Expand Up @@ -347,6 +384,30 @@ public function test_add_media_to_local_skips_php_url_without_downloading() {
$this->assertFileDoesNotExist( $malicious );
}

public function test_add_media_to_local_skips_svg_url_without_downloading() {
$theme_assets_dir = get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'assets' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR;
$expected_path = $theme_assets_dir . 'logo.svg';

if ( file_exists( $expected_path ) ) {
unlink( $expected_path );
}

$attempted = false;
// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$tracker = function ( $preempt, $args, $url ) use ( &$attempted ) {
$attempted = true;
return new WP_Error( 'cbt_test_intercept', 'blocked by test' );
};
add_filter( 'pre_http_request', $tracker, 10, 3 );

CBT_Theme_Media::add_media_to_local( array( 'http://example.com/logo.svg' ) );

remove_filter( 'pre_http_request', $tracker, 10 );

$this->assertFalse( $attempted, 'download_url() must NOT be called for an SVG URL' );
$this->assertFileDoesNotExist( $expected_path );
}

public function test_is_allowed_media_url_rejects_multi_extension_polyglots() {
$urls = array(
'http://example.com/evil.php.jpg',
Expand Down
21 changes: 21 additions & 0 deletions tests/test-theme-zip.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,27 @@ public function test_add_media_to_zip_skips_php_url_without_downloading() {
$this->assertFalse( $attempted, 'download_url() must NOT be called for a disallowed-extension URL' );
}

public function test_add_media_to_zip_skips_svg_url_without_downloading() {
list( $zip, $tmp_path ) = $this->make_temp_zip( 'cbt-test' );

$attempted = false;
// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$tracker = function ( $preempt, $args, $url ) use ( &$attempted ) {
$attempted = true;
return new WP_Error( 'cbt_test_intercept', 'blocked by test' );
};
add_filter( 'pre_http_request', $tracker, 10, 3 );

$added_media = CBT_Theme_Zip::add_media_to_zip( $zip, array( 'http://example.com/logo.svg' ) );

remove_filter( 'pre_http_request', $tracker, 10 );
$zip->close();
@unlink( $tmp_path );

$this->assertSame( array(), $added_media, 'SVG URLs should not be reported as added to the ZIP.' );
$this->assertFalse( $attempted, 'download_url() must NOT be called for an SVG URL' );
}

public function test_add_media_to_zip_preserves_downloaded_file_until_close() {
list( $zip, $tmp_path ) = $this->make_temp_zip( 'cbt-test' );

Expand Down
Loading