Make WordPress Core

Changeset 63792


Ignore:
Timestamp:
09/22/2026 12:04:48 PM (less than one hour ago)
Author:
johnbillion
Message:

Themes: Restrict path traversal in locate_template().

Props jeremyfelt, marcs0h, vortfu, jorbin, fiocavallari, xknown, sirlouen, joemcgill, pypwalters, swissspidy, shailu25, oglekler, peterwilsoncc, mukesh27, audrasjb, wildworks, rajinsharwar, martinkrcho, jeffpaul, ressl.

Fixes #58905.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/template.php

    r63351 r63792  
    491491        if ( $pagename ) {
    492492                $pagename_decoded = urldecode( $pagename );
    493                 if ( $pagename_decoded !== $pagename ) {
     493                if ( $pagename_decoded !== $pagename && 0 === validate_file( $pagename_decoded ) ) {
    494494                        $templates[] = "page-{$pagename_decoded}.php";
    495495                }
     
    700700
    701701/**
     702 * Determines whether a template found by locate_template() may be loaded.
     703 *
     704 * @since 7.1.2
     705 * @access private
     706 *
     707 * @global string $wp_stylesheet_path Path to current theme's stylesheet directory.
     708 * @global string $wp_template_path   Path to current theme's template directory.
     709 *
     710 * @param string $path Path to an existing template file.
     711 * @return bool Whether the template may be loaded.
     712 */
     713function _wp_is_template_path_allowed( $path ) {
     714        global $wp_stylesheet_path, $wp_template_path;
     715
     716        // A file path that exists and does not contain `..` is allowed.
     717        if ( 0 === preg_match( '#(?:^|/)\.\.[. ]*(?:/|$)#', wp_normalize_path( $path ) ) ) {
     718                return true;
     719        }
     720
     721        // Resolve the true location of the requested file for later comparison.
     722        $real_path = realpath( $path );
     723
     724        if ( false === $real_path ) {
     725                return false;
     726        }
     727
     728        $real_path = trailingslashit( wp_normalize_path( $real_path ) );
     729
     730        $directories = array(
     731                $wp_stylesheet_path,
     732                $wp_template_path,
     733                ABSPATH . WPINC . '/theme-compat',
     734        );
     735
     736        // If a theme is in a subdirectory, accept templates from its direct parent directory.
     737        if ( str_contains( get_stylesheet(), '/' ) ) {
     738                $directories[] = dirname( $wp_stylesheet_path );
     739        }
     740
     741        // If a parent theme is in a subdirectory, accept templates from its direct parent directory.
     742        if ( str_contains( get_template(), '/' ) ) {
     743                $directories[] = dirname( $wp_template_path );
     744        }
     745
     746        foreach ( $directories as $directory ) {
     747                $real_directory = realpath( $directory );
     748
     749                if ( false === $real_directory ) {
     750                        continue;
     751                }
     752
     753                // The true location of the requested file must be inside one of the allowed directories.
     754                if ( str_starts_with( $real_path, trailingslashit( wp_normalize_path( $real_directory ) ) ) ) {
     755                        return true;
     756                }
     757        }
     758
     759        return false;
     760}
     761
     762/**
    702763 * Retrieves the name of the highest priority template file that exists.
    703764 *
     
    708769 * @since 2.7.0
    709770 * @since 5.5.0 The `$args` parameter was added.
     771 * @since 7.1.2 A template name containing `..` is only located if it resolves inside the theme.
    710772 *
    711773 * @global string $wp_stylesheet_path Path to current theme's stylesheet directory.
     
    735797                }
    736798                if ( file_exists( $wp_stylesheet_path . '/' . $template_name ) ) {
    737                         $located = $wp_stylesheet_path . '/' . $template_name;
    738                         break;
     799                        $candidate = $wp_stylesheet_path . '/' . $template_name;
    739800                } elseif ( $is_child_theme && file_exists( $wp_template_path . '/' . $template_name ) ) {
    740                         $located = $wp_template_path . '/' . $template_name;
    741                         break;
     801                        $candidate = $wp_template_path . '/' . $template_name;
    742802                } elseif ( file_exists( ABSPATH . WPINC . '/theme-compat/' . $template_name ) ) {
    743                         $located = ABSPATH . WPINC . '/theme-compat/' . $template_name;
     803                        $candidate = ABSPATH . WPINC . '/theme-compat/' . $template_name;
     804                } else {
     805                        continue;
     806                }
     807
     808                if ( _wp_is_template_path_allowed( $candidate ) ) {
     809                        $located = $candidate;
    744810                        break;
    745811                }
Note: See TracChangeset for help on using the changeset viewer.