@@ -441,7 +441,7 @@ function get_page_template() {
441441 }
442442 if ( $ pagename ) {
443443 $ pagename_decoded = urldecode ( $ pagename );
444- if ( $ pagename_decoded !== $ pagename ) {
444+ if ( $ pagename_decoded !== $ pagename && 0 === validate_file ( $ pagename_decoded ) ) {
445445 $ templates [] = "page- {$ pagename_decoded }.php " ;
446446 }
447447 $ templates [] = "page- {$ pagename }.php " ;
@@ -636,13 +636,70 @@ function get_attachment_template() {
636636 return get_query_template ( 'attachment ' , $ templates );
637637}
638638
639+ /**
640+ * Determines whether a template found by locate_template() may be loaded.
641+ *
642+ * @since 7.1.2
643+ * @access private
644+ *
645+ * @param string $path Path to an existing template file.
646+ * @return bool Whether the template may be loaded.
647+ */
648+ function _wp_is_template_path_allowed ( $ path ) {
649+ // A file path that exists and does not contain `..` is allowed.
650+ if ( 0 === preg_match ( '#(?:^|/)\.\.[. ]*(?:/|$)# ' , wp_normalize_path ( $ path ) ) ) {
651+ return true ;
652+ }
653+
654+ // Resolve the true location of the requested file for later comparison.
655+ $ real_path = realpath ( $ path );
656+
657+ if ( false === $ real_path ) {
658+ return false ;
659+ }
660+
661+ $ real_path = trailingslashit ( wp_normalize_path ( $ real_path ) );
662+
663+ $ directories = array (
664+ STYLESHEETPATH ,
665+ TEMPLATEPATH ,
666+ ABSPATH . WPINC . '/theme-compat ' ,
667+ );
668+
669+ // If a theme is in a subdirectory, accept templates from its direct parent directory.
670+ if ( false !== strpos ( get_stylesheet (), '/ ' ) ) {
671+ $ directories [] = dirname ( STYLESHEETPATH );
672+ }
673+
674+ // If a parent theme is in a subdirectory, accept templates from its direct parent directory.
675+ if ( false !== strpos ( get_template (), '/ ' ) ) {
676+ $ directories [] = dirname ( TEMPLATEPATH );
677+ }
678+
679+ foreach ( $ directories as $ directory ) {
680+ $ real_directory = realpath ( $ directory );
681+
682+ if ( false === $ real_directory ) {
683+ continue ;
684+ }
685+
686+ // The true location of the requested file must be inside one of the allowed directories.
687+ if ( str_starts_with ( $ real_path , trailingslashit ( wp_normalize_path ( $ real_directory ) ) ) ) {
688+ return true ;
689+ }
690+ }
691+
692+ return false ;
693+ }
694+
639695/**
640696 * Retrieve the name of the highest priority template file that exists.
641697 *
642698 * Searches in the STYLESHEETPATH before TEMPLATEPATH and wp-includes/theme-compat
643699 * so that themes which inherit from a parent theme can just overload one file.
644700 *
645701 * @since 2.7.0
702+ * @since 7.1.2 A template name containing `..` is only located if it resolves inside the theme.
646703 *
647704 * @param string|array $template_names Template file(s) to search for, in order.
648705 * @param bool $load If true the template file will be loaded if it is found.
@@ -656,13 +713,17 @@ function locate_template( $template_names, $load = false, $require_once = true )
656713 continue ;
657714 }
658715 if ( file_exists ( STYLESHEETPATH . '/ ' . $ template_name ) ) {
659- $ located = STYLESHEETPATH . '/ ' . $ template_name ;
660- break ;
716+ $ candidate = STYLESHEETPATH . '/ ' . $ template_name ;
661717 } elseif ( file_exists ( TEMPLATEPATH . '/ ' . $ template_name ) ) {
662- $ located = TEMPLATEPATH . '/ ' . $ template_name ;
663- break ;
718+ $ candidate = TEMPLATEPATH . '/ ' . $ template_name ;
664719 } elseif ( file_exists ( ABSPATH . WPINC . '/theme-compat/ ' . $ template_name ) ) {
665- $ located = ABSPATH . WPINC . '/theme-compat/ ' . $ template_name ;
720+ $ candidate = ABSPATH . WPINC . '/theme-compat/ ' . $ template_name ;
721+ } else {
722+ continue ;
723+ }
724+
725+ if ( _wp_is_template_path_allowed ( $ candidate ) ) {
726+ $ located = $ candidate ;
666727 break ;
667728 }
668729 }
0 commit comments