diff --git a/includes/create-theme/theme-media.php b/includes/create-theme/theme-media.php index f29d57c8..0f7ead6c 100644 --- a/includes/create-theme/theme-media.php +++ b/includes/create-theme/theme-media.php @@ -108,7 +108,6 @@ public static function is_allowed_media_url( $url ) { 'jpeg', 'png', 'gif', - 'svg', 'webp', 'avif', // videos @@ -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 → `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 ); @@ -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' ); } @@ -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( '', 'http://example.com/cat.svg' ); + public function test_is_allowed_media_file_rejects_svg_content() { + $this->assert_media_magic_rejected( '', '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"?' . '>' . ''; - $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() { @@ -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 = ' @@ -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 = ' + +
+ + '; + + $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 = ' @@ -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', diff --git a/tests/test-theme-zip.php b/tests/test-theme-zip.php index 16c9ab72..6a1638ce 100644 --- a/tests/test-theme-zip.php +++ b/tests/test-theme-zip.php @@ -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' );