@@ -442,7 +442,7 @@ function get_page_template() {
442442 }
443443 if ( $ pagename ) {
444444 $ pagename_decoded = urldecode ( $ pagename );
445- if ( $ pagename_decoded !== $ pagename ) {
445+ if ( $ pagename_decoded !== $ pagename && 0 === validate_file ( $ pagename_decoded ) ) {
446446 $ templates [] = "page- {$ pagename_decoded }.php " ;
447447 }
448448 $ templates [] = "page- {$ pagename }.php " ;
@@ -637,6 +637,62 @@ function get_attachment_template() {
637637 return get_query_template ( 'attachment ' , $ templates );
638638}
639639
640+ /**
641+ * Determines whether a template found by locate_template() may be loaded.
642+ *
643+ * @since 7.1.2
644+ * @access private
645+ *
646+ * @param string $path Path to an existing template file.
647+ * @return bool Whether the template may be loaded.
648+ */
649+ function _wp_is_template_path_allowed ( $ path ) {
650+ // A file path that exists and does not contain `..` is allowed.
651+ if ( 0 === preg_match ( '#(?:^|/)\.\.[. ]*(?:/|$)# ' , wp_normalize_path ( $ path ) ) ) {
652+ return true ;
653+ }
654+
655+ // Resolve the true location of the requested file for later comparison.
656+ $ real_path = realpath ( $ path );
657+
658+ if ( false === $ real_path ) {
659+ return false ;
660+ }
661+
662+ $ real_path = trailingslashit ( wp_normalize_path ( $ real_path ) );
663+
664+ $ directories = array (
665+ STYLESHEETPATH ,
666+ TEMPLATEPATH ,
667+ ABSPATH . WPINC . '/theme-compat ' ,
668+ );
669+
670+ // If a theme is in a subdirectory, accept templates from its direct parent directory.
671+ if ( false !== strpos ( get_stylesheet (), '/ ' ) ) {
672+ $ directories [] = dirname ( STYLESHEETPATH );
673+ }
674+
675+ // If a parent theme is in a subdirectory, accept templates from its direct parent directory.
676+ if ( false !== strpos ( get_template (), '/ ' ) ) {
677+ $ directories [] = dirname ( TEMPLATEPATH );
678+ }
679+
680+ foreach ( $ directories as $ directory ) {
681+ $ real_directory = realpath ( $ directory );
682+
683+ if ( false === $ real_directory ) {
684+ continue ;
685+ }
686+
687+ // The true location of the requested file must be inside one of the allowed directories.
688+ if ( str_starts_with ( $ real_path , trailingslashit ( wp_normalize_path ( $ real_directory ) ) ) ) {
689+ return true ;
690+ }
691+ }
692+
693+ return false ;
694+ }
695+
640696/**
641697 * Retrieve the name of the highest priority template file that exists.
642698 *
@@ -645,6 +701,7 @@ function get_attachment_template() {
645701 *
646702 * @since 2.7.0
647703 * @since 5.5.0 The `$args` parameter was added.
704+ * @since 7.1.2 A template name containing `..` is only located if it resolves inside the theme.
648705 *
649706 * @param string|array $template_names Template file(s) to search for, in order.
650707 * @param bool $load If true the template file will be loaded if it is found.
@@ -661,13 +718,17 @@ function locate_template( $template_names, $load = false, $require_once = true,
661718 continue ;
662719 }
663720 if ( file_exists ( STYLESHEETPATH . '/ ' . $ template_name ) ) {
664- $ located = STYLESHEETPATH . '/ ' . $ template_name ;
665- break ;
721+ $ candidate = STYLESHEETPATH . '/ ' . $ template_name ;
666722 } elseif ( file_exists ( TEMPLATEPATH . '/ ' . $ template_name ) ) {
667- $ located = TEMPLATEPATH . '/ ' . $ template_name ;
668- break ;
723+ $ candidate = TEMPLATEPATH . '/ ' . $ template_name ;
669724 } elseif ( file_exists ( ABSPATH . WPINC . '/theme-compat/ ' . $ template_name ) ) {
670- $ located = ABSPATH . WPINC . '/theme-compat/ ' . $ template_name ;
725+ $ candidate = ABSPATH . WPINC . '/theme-compat/ ' . $ template_name ;
726+ } else {
727+ continue ;
728+ }
729+
730+ if ( _wp_is_template_path_allowed ( $ candidate ) ) {
731+ $ located = $ candidate ;
671732 break ;
672733 }
673734 }
0 commit comments