Make WordPress Core

Changeset 50132


Ignore:
Timestamp:
02/02/2021 12:38:40 AM (4 years ago)
Author:
peterwilsoncc
Message:

Canonical: Prevent ID enumeration of private post slugs.

Add check to redirect_canonical() to ensure private posts only redirect for logged in users.

Modifies the read_post mata capability to user get_post_status() rather than the post's post_status property to allow attachments to redirect based on the inherited post status.

Introduces wp_force_ugly_post_permalink() to unify the check to determine if an ugly link should be displayed in each of the functions used for determining permalinks: get_permalink(), get_post_permalink(), _get_page_link() and get_attachment_link().

Improves logic of get_attachment_link() to validate parent post and resolution of inherited post status. This is an incomplete fix of #52373 to prevent the function returning links resulting in a file not found error. Required to unblock this ticket.

Props peterwilsoncc, TimothyBlynJacobs.
See #52373.
Fixes #5272.

Location:
trunk
Files:
1 added
5 edited

Legend:

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

    r49924 r50132  
    7878    $redirect     = $original;
    7979    $redirect_url = false;
     80    $redirect_obj = false;
    8081
    8182    // Notice fixing.
     
    103104    if ( is_feed() && $post_id ) {
    104105        $redirect_url = get_post_comments_feed_link( $post_id, get_query_var( 'feed' ) );
     106        $redirect_obj = get_post( $post_id );
    105107
    106108        if ( $redirect_url ) {
     
    127129
    128130            $redirect_url = get_permalink( $post_id );
     131            $redirect_obj = get_post( $post_id );
    129132
    130133            if ( $redirect_url ) {
     
    151154            if ( $post_type_obj && $post_type_obj->public && 'auto-draft' !== $redirect_post->post_status ) {
    152155                $redirect_url = get_permalink( $redirect_post );
     156                $redirect_obj = get_post( $redirect_post );
    153157
    154158                $redirect['query'] = _remove_qs_args_if_not_in_url(
     
    198202            if ( $post_id ) {
    199203                $redirect_url = get_permalink( $post_id );
     204                $redirect_obj = get_post( $post_id );
    200205
    201206                $redirect['path']  = rtrim( $redirect['path'], (int) get_query_var( 'page' ) . '/' );
     
    224229            if ( ! empty( $_GET['attachment_id'] ) ) {
    225230                $redirect_url = get_attachment_link( get_query_var( 'attachment_id' ) );
     231                $redirect_obj = get_post( get_query_var( 'attachment_id' ) );
    226232
    227233                if ( $redirect_url ) {
     
    230236            } else {
    231237                $redirect_url = get_attachment_link();
     238                $redirect_obj = get_post();
    232239            }
    233240        } elseif ( is_single() && ! empty( $_GET['p'] ) && ! $redirect_url ) {
    234241            $redirect_url = get_permalink( get_query_var( 'p' ) );
     242            $redirect_obj = get_post( get_query_var( 'p' ) );
    235243
    236244            if ( $redirect_url ) {
     
    239247        } elseif ( is_single() && ! empty( $_GET['name'] ) && ! $redirect_url ) {
    240248            $redirect_url = get_permalink( $wp_query->get_queried_object_id() );
     249            $redirect_obj = get_post( $wp_query->get_queried_object_id() );
    241250
    242251            if ( $redirect_url ) {
     
    245254        } elseif ( is_page() && ! empty( $_GET['page_id'] ) && ! $redirect_url ) {
    246255            $redirect_url = get_permalink( get_query_var( 'page_id' ) );
     256            $redirect_obj = get_post( get_query_var( 'page_id' ) );
    247257
    248258            if ( $redirect_url ) {
     
    257267        ) {
    258268            $redirect_url = get_permalink( get_option( 'page_for_posts' ) );
     269            $redirect_obj = get_post( get_option( 'page_for_posts' ) );
    259270
    260271            if ( $redirect_url ) {
     
    311322            ) {
    312323                $redirect_url = get_author_posts_url( $author->ID, $author->user_nicename );
     324                $redirect_obj = $author;
    313325
    314326                if ( $redirect_url ) {
     
    386398                ) {
    387399                    $redirect_url = get_permalink( $wp_query->get_queried_object_id() );
     400                    $redirect_obj = get_post( $wp_query->get_queried_object_id() );
    388401                }
    389402            }
     
    396409            if ( ! $redirect_url ) {
    397410                $redirect_url = get_permalink( get_queried_object_id() );
     411                $redirect_obj = get_post( get_queried_object_id() );
    398412            }
    399413
     
    741755    }
    742756
     757    if ( $redirect_obj instanceof WP_Post ) {
     758        $post_status_obj = get_post_status_object( get_post_status( $redirect_obj ) );
     759        /*
     760         * Unset the redirect object and URL if they are not readable by the user.
     761         * This condition is a little confusing as the condition needs to pass if
     762         * the post is not readable by the user. That's why there are ! (not) conditions
     763         * throughout.
     764         */
     765        if (
     766            // Private post statuses only redirect if the user can read them.
     767            ! (
     768                $post_status_obj->private &&
     769                current_user_can( 'read_post', $redirect_obj->ID )
     770            ) &&
     771            // For other posts, only redirect if publicly viewable.
     772            ! is_post_publicly_viewable( $redirect_obj )
     773        ) {
     774            $redirect_obj = false;
     775            $redirect_url = false;
     776        }
     777    }
     778
    743779    /**
    744780     * Filters the canonical redirect URL.
  • trunk/src/wp-includes/capabilities.php

    r50131 r50132  
    246246            }
    247247
    248             $status_obj = get_post_status_object( $post->post_status );
     248            $status_obj = get_post_status_object( get_post_status( $post ) );
    249249            if ( ! $status_obj ) {
    250250                /* translators: 1: Post status, 2: Capability name. */
    251                 _doing_it_wrong( __FUNCTION__, sprintf( __( 'The post status %1$s is not registered, so it may not be reliable to check the capability "%2$s" against a post with that status.' ), $post->post_status, $cap ), '5.4.0' );
     251                _doing_it_wrong( __FUNCTION__, sprintf( __( 'The post status %1$s is not registered, so it may not be reliable to check the capability "%2$s" against a post with that status.' ), get_post_status( $post ), $cap ), '5.4.0' );
    252252                $caps[] = 'edit_others_posts';
    253253                break;
  • trunk/src/wp-includes/link-template.php

    r49789 r50132  
    8888            break;
    8989    }
     90}
     91
     92/**
     93 * Determine whether post should always use an ugly permalink structure.
     94 *
     95 * @since 5.7.0
     96 *
     97 * @param WP_Post|int|null $post   Optional. Post ID or post object. Defaults to global $post.
     98 * @param bool|null        $sample Optional. Whether to force consideration based on sample links.
     99 *                                 If omitted, a sample link is generated if a post object is passed
     100 *                                 with the filter property set to 'sample'.
     101 * @return bool Whether to use an ugly permalink structure.
     102 */
     103function wp_force_ugly_post_permalink( $post = null, $sample = null ) {
     104    if (
     105        null === $sample &&
     106        is_object( $post ) &&
     107        isset( $post->filter ) &&
     108        'sample' === $post->filter
     109    ) {
     110        $sample = true;
     111    } else {
     112        $post   = get_post( $post );
     113        $sample = null !== $sample ? $sample : false;
     114    }
     115
     116    if ( ! $post ) {
     117        return true;
     118    }
     119
     120    $post_status_obj = get_post_status_object( get_post_status( $post ) );
     121    $post_type_obj   = get_post_type_object( get_post_type( $post ) );
     122
     123    if ( ! $post_status_obj || ! $post_type_obj ) {
     124        return true;
     125    }
     126
     127    if (
     128        // Publicly viewable links never have ugly permalinks.
     129        is_post_status_viewable( $post_status_obj ) ||
     130        (
     131            // Private posts don't have ugly links if the user can read them.
     132            $post_status_obj->private &&
     133            current_user_can( 'read_post', $post->ID )
     134        ) ||
     135        // Protected posts don't have ugly links if getting a sample URL.
     136        ( $post_status_obj->protected && $sample )
     137    ) {
     138        return false;
     139    }
     140
     141    return true;
    90142}
    91143
     
    167219    if (
    168220        $permalink &&
    169         ! in_array( $post->post_status, array( 'draft', 'pending', 'auto-draft', 'future', 'trash' ), true )
     221        ! wp_force_ugly_post_permalink( $post )
    170222    ) {
    171223
     
    278330    $slug = $post->post_name;
    279331
    280     $draft_or_pending = get_post_status( $post ) && in_array( get_post_status( $post ), array( 'draft', 'pending', 'auto-draft', 'future' ), true );
     332    $force_ugly_link = wp_force_ugly_post_permalink( $post );
    281333
    282334    $post_type = get_post_type_object( $post->post_type );
     
    286338    }
    287339
    288     if ( ! empty( $post_link ) && ( ! $draft_or_pending || $sample ) ) {
     340    if ( ! empty( $post_link ) && ( ! $force_ugly_link || $sample ) ) {
    289341        if ( ! $leavename ) {
    290342            $post_link = str_replace( "%$post->post_type%", $slug, $post_link );
     
    292344        $post_link = home_url( user_trailingslashit( $post_link ) );
    293345    } else {
    294         if ( $post_type->query_var && ( isset( $post->post_status ) && ! $draft_or_pending ) ) {
     346        if ( $post_type->query_var && ( isset( $post->post_status ) && ! $force_ugly_link ) ) {
    295347            $post_link = add_query_arg( $post_type->query_var, $slug, '' );
    296348        } else {
     
    374426    $post = get_post( $post );
    375427
    376     $draft_or_pending = in_array( $post->post_status, array( 'draft', 'pending', 'auto-draft' ), true );
     428    $force_ugly_link = wp_force_ugly_post_permalink( $post );
    377429
    378430    $link = $wp_rewrite->get_page_permastruct();
    379431
    380     if ( ! empty( $link ) && ( ( isset( $post->post_status ) && ! $draft_or_pending ) || $sample ) ) {
     432    if ( ! empty( $link ) && ( ( isset( $post->post_status ) && ! $force_ugly_link ) || $sample ) ) {
    381433        if ( ! $leavename ) {
    382434            $link = str_replace( '%pagename%', get_page_uri( $post ), $link );
     
    418470    $link = false;
    419471
    420     $post   = get_post( $post );
    421     $parent = ( $post->post_parent > 0 && $post->post_parent != $post->ID ) ? get_post( $post->post_parent ) : false;
    422     if ( $parent && ! in_array( $parent->post_type, get_post_types(), true ) ) {
    423         $parent = false;
    424     }
    425 
    426     if ( $wp_rewrite->using_permalinks() && $parent ) {
     472    $post            = get_post( $post );
     473    $force_ugly_link = wp_force_ugly_post_permalink( $post );
     474    $parent_id       = $post->post_parent;
     475    $parent          = $parent_id ? get_post( $parent_id ) : false;
     476    $parent_valid    = true; // Default for no parent.
     477    if (
     478        $parent_id &&
     479        (
     480            $post->post_parent === $post->ID ||
     481            ! $parent ||
     482            ! is_post_type_viewable( get_post_type( $parent ) )
     483        )
     484    ) {
     485        // Post is either its own parent or parent post unavailable.
     486        $parent_valid = false;
     487    }
     488
     489    if ( $force_ugly_link || ! $parent_valid ) {
     490        $link = false;
     491    } elseif ( $wp_rewrite->using_permalinks() && $parent ) {
    427492        if ( 'page' === $parent->post_type ) {
    428493            $parentlink = _get_page_link( $post->post_parent ); // Ignores page_on_front.
  • trunk/tests/phpunit/tests/link.php

    r48937 r50132  
    205205        }
    206206
    207         $this->assertSame( home_url( user_trailingslashit( $attachment->post_name ) ), get_permalink( $attachment_id ) );
     207        $this->assertSame( home_url( "/?attachment_id={$attachment->ID}" ), get_permalink( $attachment_id ) );
     208        // Visit permalink.
     209        $this->go_to( get_permalink( $attachment_id ) );
     210        $this->assertQueryTrue( 'is_attachment', 'is_single', 'is_singular' );
    208211    }
    209212}
  • trunk/tests/phpunit/tests/media.php

    r49974 r50132  
    31233123     *
    31243124     * @param string $post_key     Post as keyed in the shared fixture array.
    3125      * @param string $expected     Expected result.
     3125     * @param string $expected_url Expected permalink.
    31263126     * @param bool   $expected_404 Whether the page is expected to return a 404 result.
    31273127     *
    31283128     */
    3129     function test_attachment_permalinks_based_on_parent_status( $post_key, $expected, $expected_404 ) {
     3129    function test_attachment_permalinks_based_on_parent_status( $post_key, $expected_url, $expected_404 ) {
    31303130        $this->set_permalink_structure( '/%postname%' );
    31313131        $post = get_post( self::$post_ids[ $post_key ] );
     
    31353135         * post object IDs are placeholders that needs to be replaced.
    31363136         */
    3137         $expected = home_url( str_replace( '%ID%', $post->ID, $expected ) );
    3138 
    3139         $this->assertSame( $expected, get_permalink( $post ) );
     3137        $expected_url = home_url( str_replace( '%ID%', $post->ID, $expected_url ) );
     3138
    31403139        $this->go_to( get_permalink( $post ) );
    3141         $this->assertSame( $expected_404, is_404() );
     3140        $this->assertSame( $expected_url, get_permalink( $post ) );
     3141        if ( $expected_404 ) {
     3142            $this->assertQueryTrue( 'is_404' );
     3143        } else {
     3144            $this->assertQueryTrue( 'is_attachment', 'is_single', 'is_singular' );
     3145        }
     3146        $this->assertSame( 'attachment', $post->post_type );
    31423147    }
    31433148
     
    31473152     * @return array[] {
    31483153     *     @type string $post_key     Post as keyed in the shared fixture array.
    3149      *     @type string $expected     Expected result.
     3154     *     @type string $expected_url Expected permalink.
    31503155     *     $type bool   $expected_404 Whether the page is expected to return a 404 result.
    31513156     * }
Note: See TracChangeset for help on using the changeset viewer.