Commit 860f99e83d for wordpress.org
commit 860f99e83d0e0efbb66b8fa9609be7a7d250cc1d
Author: adamsilverstein <adamsilverstein@git.wordpress.org>
Date: Thu Sep 17 17:30:45 2026 +0000
Customize: Improve `header_image_data` theme mod sanitization.
Props jeremyfelt, lancewillett, jonsurrell, westonruter, dmsnell.
Built from https://develop.svn.wordpress.org/trunk@63659
git-svn-id: http://core.svn.wordpress.org/trunk@62833 1a063a9b-81f0-0310-95a4-ce76da25c4cd
diff --git a/wp-admin/includes/class-custom-image-header.php b/wp-admin/includes/class-custom-image-header.php
index c1816ffae2..367ba04cdc 100644
--- a/wp-admin/includes/class-custom-image-header.php
+++ b/wp-admin/includes/class-custom-image-header.php
@@ -572,7 +572,7 @@ class Custom_Image_Header {
$header_image_style .= 'height:' . $custom_header->height . 'px;';
}
?>
- <div id="headimg" style="<?php echo $header_image_style; ?>">
+ <div id="headimg" style="<?php echo esc_attr( $header_image_style ); ?>">
<?php
if ( display_header_text() ) {
$style = ' style="color:#' . get_header_textcolor() . ';"';
@@ -1184,8 +1184,8 @@ endif;
'attachment_id' => $choice['attachment_id'],
'url' => $choice['url'],
'thumbnail_url' => $choice['url'],
- 'height' => $choice['height'],
- 'width' => $choice['width'],
+ 'height' => absint( $choice['height'] ),
+ 'width' => absint( $choice['width'] ),
);
update_post_meta( $choice['attachment_id'], '_wp_attachment_is_custom_header', get_stylesheet() );
@@ -1216,7 +1216,13 @@ endif;
}
}
- set_theme_mod( 'header_image', sanitize_url( $header_image_data['url'] ) );
+ $header_image_data['url'] = sanitize_url( $header_image_data['url'] );
+
+ if ( isset( $header_image_data['thumbnail_url'] ) ) {
+ $header_image_data['thumbnail_url'] = sanitize_url( $header_image_data['thumbnail_url'] );
+ }
+
+ set_theme_mod( 'header_image', $header_image_data['url'] );
set_theme_mod( 'header_image_data', $header_image_data );
}
@@ -1589,9 +1595,8 @@ endif;
$alt_text_key = '_wp_attachment_image_alt';
foreach ( $header_images as &$header_image ) {
- $header_meta = get_post_meta( $header_image['attachment_id'] );
- $header_image['timestamp'] = $header_meta[ $timestamp_key ] ?? '';
- $header_image['alt_text'] = $header_meta[ $alt_text_key ] ?? '';
+ $header_image['timestamp'] = get_post_meta( $header_image['attachment_id'], $timestamp_key, true );
+ $header_image['alt_text'] = get_post_meta( $header_image['attachment_id'], $alt_text_key, true );
}
return $header_images;
diff --git a/wp-includes/customize/class-wp-customize-header-image-setting.php b/wp-includes/customize/class-wp-customize-header-image-setting.php
index 009e2e606f..8076e1e65c 100644
--- a/wp-includes/customize/class-wp-customize-header-image-setting.php
+++ b/wp-includes/customize/class-wp-customize-header-image-setting.php
@@ -15,6 +15,17 @@
* @since 3.4.0
*
* @see WP_Customize_Setting
+ *
+ * @phpstan-type Header_Image_Data array{
+ * attachment_id?: int,
+ * url?: string,
+ * thumbnail_url?: string,
+ * timestamp?: int,
+ * width?: int,
+ * height?: int,
+ * alt_text?: string,
+ * attachment_parent?: int,
+ * }
*/
final class WP_Customize_Header_Image_Setting extends WP_Customize_Setting {
@@ -62,4 +73,163 @@ final class WP_Customize_Header_Image_Setting extends WP_Customize_Setting {
}
return true;
}
+
+ /**
+ * Sanitizes a header value.
+ *
+ * The value is expected to be one of the following:
+ *
+ * - An array of header image data, with the keys `attachment_id`, `url`, `thumbnail_url`, `timestamp`, `width`,
+ * `height`, `alt_text`, and `attachment_parent`, as supplied by {@see get_uploaded_header_images()}. Any other
+ * key is discarded.
+ * - An array with a `choice` key, being the legacy format in which any of the other accepted values is nested.
+ * - The string `remove-header`, `random-default-image`, or `random-uploaded-image`.
+ * - A string corresponding to one of the keys for the array returned by {@see get_uploaded_header_images()}, or
+ * one of the keys for the array passed into {@see register_default_headers()}.
+ *
+ * @since 7.1.1
+ *
+ * @see WP_Customize_Header_Image_Setting::update()
+ * @see Custom_Image_Header::set_header_image()
+ *
+ * @param mixed $value Value to sanitize.
+ * @return array|string|WP_Error|null Sanitized value, or `null`/`WP_Error` if invalid. The array holds
+ * the header image data, or that data nested under a `choice` key,
+ * before the `customize_sanitize_header_image_data` filter, which
+ * may return anything, is applied to it.
+ *
+ * @phpstan-return array<mixed, mixed>|string|WP_Error|null
+ */
+ public function sanitize( $value ) {
+ /*
+ * The update() method unwraps the legacy `choice` format before handing the value off to
+ * Custom_Image_Header::set_header_image(), so the nested value is what must be sanitized.
+ */
+ if ( is_array( $value ) && isset( $value['choice'] ) ) {
+ $choice = $this->sanitize_choice( $value['choice'] );
+ if ( is_null( $choice ) || is_wp_error( $choice ) ) {
+ return $choice;
+ }
+ $value = array( 'choice' => $choice );
+ } else {
+ $value = $this->sanitize_choice( $value );
+ if ( is_null( $value ) || is_wp_error( $value ) ) {
+ return $value;
+ }
+ }
+
+ return parent::sanitize( $value );
+ }
+
+ /**
+ * Sanitizes a header image choice.
+ *
+ * This is the value which is ultimately passed to {@see Custom_Image_Header::set_header_image()}, whether
+ * supplied at the top level of the setting value or nested under its legacy `choice` key.
+ *
+ * @since 7.1.1
+ *
+ * @param mixed $value Value to sanitize.
+ * @return array|string|WP_Error|null Sanitized value, or `null`/`WP_Error` if invalid.
+ *
+ * @phpstan-return Header_Image_Data|string|WP_Error|null
+ */
+ private function sanitize_choice( $value ) {
+ // Custom_Image_Header::set_header_image() accepts an object in place of an array.
+ if ( is_object( $value ) ) {
+ $value = (array) $value;
+ }
+
+ if ( is_string( $value ) ) {
+ return sanitize_text_field( $value );
+ }
+
+ if ( ! is_array( $value ) ) {
+ return null;
+ }
+
+ /*
+ * The sanitized value is assembled member by member rather than filtered down from the
+ * supplied one, so that nothing but the members below can end up in it.
+ */
+ $sanitized = array();
+
+ if ( isset( $value['attachment_id'] ) ) {
+ if ( ! is_scalar( $value['attachment_id'] ) ) {
+ return null;
+ }
+ $attachment_id = absint( $value['attachment_id'] );
+
+ /*
+ * A supplied attachment must be an existing image, since its ID is written to postmeta and its
+ * data displayed. Note that an ID of zero must be skipped rather than looked up, as
+ * get_post_mime_type() falls back to the global post when passed an empty value.
+ */
+ if ( $attachment_id > 0 ) {
+ $mime_type = get_post_mime_type( $attachment_id );
+ if ( ! is_string( $mime_type ) || ! str_starts_with( $mime_type, 'image/' ) ) {
+ return null;
+ }
+ }
+
+ $sanitized['attachment_id'] = $attachment_id;
+ }
+
+ if ( isset( $value['url'] ) ) {
+ if ( ! is_string( $value['url'] ) ) {
+ return null;
+ }
+ $sanitized['url'] = sanitize_url( $value['url'] );
+ if ( '' === $sanitized['url'] ) {
+ return new WP_Error( 'invalid_url', __( 'Invalid URL.' ) );
+ }
+ }
+
+ if ( isset( $value['thumbnail_url'] ) ) {
+ if ( ! is_string( $value['thumbnail_url'] ) ) {
+ return null;
+ }
+ $sanitized['thumbnail_url'] = sanitize_url( $value['thumbnail_url'] );
+ if ( '' === $sanitized['thumbnail_url'] ) {
+ return new WP_Error( 'invalid_url', __( 'Invalid URL.' ) );
+ }
+ }
+
+ if ( isset( $value['timestamp'] ) ) {
+ if ( ! is_scalar( $value['timestamp'] ) ) {
+ return null;
+ }
+ $sanitized['timestamp'] = absint( $value['timestamp'] );
+ }
+
+ if ( isset( $value['width'] ) ) {
+ if ( ! is_scalar( $value['width'] ) ) {
+ return null;
+ }
+ $sanitized['width'] = absint( $value['width'] );
+ }
+
+ if ( isset( $value['height'] ) ) {
+ if ( ! is_scalar( $value['height'] ) ) {
+ return null;
+ }
+ $sanitized['height'] = absint( $value['height'] );
+ }
+
+ if ( isset( $value['alt_text'] ) ) {
+ if ( ! is_string( $value['alt_text'] ) ) {
+ return null;
+ }
+ $sanitized['alt_text'] = sanitize_text_field( $value['alt_text'] );
+ }
+
+ if ( isset( $value['attachment_parent'] ) ) {
+ if ( ! is_scalar( $value['attachment_parent'] ) ) {
+ return null;
+ }
+ $sanitized['attachment_parent'] = absint( $value['attachment_parent'] );
+ }
+
+ return $sanitized;
+ }
}
diff --git a/wp-includes/theme.php b/wp-includes/theme.php
index 9c18c71792..d81616c6c5 100644
--- a/wp-includes/theme.php
+++ b/wp-includes/theme.php
@@ -1551,6 +1551,7 @@ function get_uploaded_header_images() {
* Gets the header image data.
*
* @since 3.4.0
+ * @since 7.1.1 The `width` and `height` are cast to non-negative integers.
*
* @global array $_wp_default_headers
*
@@ -1589,7 +1590,14 @@ function get_custom_header() {
'height' => get_theme_support( 'custom-header', 'height' ),
'video' => get_theme_support( 'custom-header', 'video' ),
);
- return (object) wp_parse_args( $data, $default );
+
+ if ( ! is_array( $data ) && ! is_object( $data ) ) {
+ $data = array();
+ }
+ $header = (object) wp_parse_args( $data, $default );
+ $header->width = absint( $header->width );
+ $header->height = absint( $header->height );
+ return $header;
}
/**
diff --git a/wp-includes/version.php b/wp-includes/version.php
index 91a2f68d24..f5b3b1998f 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
-$wp_version = '7.2-alpha-63658';
+$wp_version = '7.2-alpha-63659';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.