Make WordPress Core

Changeset 63036 for trunk


Ignore:
Timestamp:
08/06/2026 09:30:58 AM (4 days ago)
Author:
youknowriad
Message:

Pings/Trackbacks: Auto-approve pingbacks and trackbacks from the same site.

Pings carry no email address, so they never satisfy the
comment_previously_approved option, which is enabled by default. Every ping
is held for moderation indefinitely as a result, including the ones a site
sends to itself when a new post links to an older one.

Approve a ping whose source URL resolves to a published post on this site.
Existing moderation checks still take precedence, and the new
auto_approve_ping filter controls the decision.

Props annezazu, avcascade, chriscct7, desrosj, dshanske, eurello, matt,
mohkatz, SergeyBiryukov, stevegrunwell.
Fixes #65016.

Location:
trunk
Files:
3 edited

Legend:

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

    r63012 r63036  
    2121 * approved.
    2222 *
     23 * Pingbacks originating from this site are automatically approved, as the link
     24 * they report was created by someone who can already publish here.
     25 *
    2326 * If all checks pass, the function will return true.
    2427 *
    2528 * @since 1.2.0
     29 * @since 7.2.0 Pingbacks from this site are no longer held for moderation.
    2630 *
    2731 * @global wpdb $wpdb WordPress database abstraction object.
     
    162166                                return false;
    163167                        }
     168                } elseif ( 'pingback' === $comment_type ) {
     169                        /*
     170                         * Only pingbacks are considered. A pingback is verified before it reaches
     171                         * this point: the source page is fetched, it must link to the target, and
     172                         * the comment is built from that fetched page. A trackback carries no such
     173                         * proof. Its source URL, title, and excerpt are unverified request data, so
     174                         * a forged trackback naming a local post as its source would be approved.
     175                         */
     176
     177                        // url_to_postid() compares hostnames, so it returns 0 for any URL that only appears to be local.
     178                        $source_id = url_to_postid( wp_unslash( $url ) );
     179
     180                        // Approve pingbacks reporting a link that someone who can already publish here created.
     181                        $approve_pingback = $source_id > 0 && 'publish' === get_post_status( $source_id );
     182
     183                        /**
     184                         * Filters whether a pingback is approved without being held for moderation.
     185                         *
     186                         * Defaults to true for pingbacks originating from a published post on this
     187                         * site, and false for every other pingback. Trackbacks are never considered,
     188                         * as they cannot be verified.
     189                         *
     190                         * @since 7.2.0
     191                         *
     192                         * @param bool   $approve_pingback Whether to approve the pingback.
     193                         * @param int    $source_id        ID of the post on this site the pingback
     194                         *                                 originated from, or 0 if it came from elsewhere.
     195                         * @param string $url              The URL the pingback was sent from.
     196                         */
     197                        return (bool) apply_filters( 'auto_approve_pingback', $approve_pingback, $source_id, $url );
    164198                } else {
    165199                        return false;
  • trunk/tests/phpunit/tests/comment/checkComment.php

    r56548 r63036  
    206206                $this->assertFalse( $results );
    207207        }
     208
     209        /**
     210         * @ticket 65016
     211         */
     212        public function test_should_return_true_for_a_pingback_from_this_site() {
     213                update_option( 'comment_previously_approved', '1' );
     214
     215                $source_url = get_permalink( self::factory()->post->create() );
     216
     217                $this->assertTrue( check_comment( 'Site Title', '', $source_url, 'Excerpt.', '192.168.0.1', '', 'pingback' ) );
     218        }
     219
     220        /**
     221         * Trackbacks are never approved automatically.
     222         *
     223         * A trackback's source URL, title, and excerpt are unverified request data. Were
     224         * they trusted, anyone could POST a trackback naming a local post as its source
     225         * and have arbitrary content approved without moderation.
     226         *
     227         * @ticket 65016
     228         */
     229        public function test_should_return_false_for_a_trackback_claiming_a_local_source() {
     230                update_option( 'comment_previously_approved', '1' );
     231
     232                $source_url = get_permalink( self::factory()->post->create() );
     233
     234                $this->assertFalse( check_comment( 'ForgedSite', '', $source_url, 'Spam.', '192.168.0.1', '', 'trackback' ) );
     235        }
     236
     237        /**
     238         * @ticket 65016
     239         *
     240         * @dataProvider data_ping_types
     241         *
     242         * @param string $comment_type The comment type.
     243         */
     244        public function test_should_return_false_for_a_ping_from_another_site( $comment_type ) {
     245                update_option( 'comment_previously_approved', '1' );
     246
     247                $this->assertFalse( check_comment( 'Site Title', '', 'http://example.com/a-post/', 'Excerpt.', '192.168.0.1', '', $comment_type ) );
     248        }
     249
     250        /**
     251         * A URL is only local when its host matches, not when it merely contains the home URL.
     252         *
     253         * @ticket 65016
     254         *
     255         * @dataProvider data_ping_types
     256         *
     257         * @param string $comment_type The comment type.
     258         */
     259        public function test_should_return_false_for_a_ping_from_a_url_spoofing_this_site( $comment_type ) {
     260                update_option( 'comment_previously_approved', '1' );
     261
     262                $post_id    = self::factory()->post->create();
     263                $source_url = 'http://example.com/?ref=' . rawurlencode( get_permalink( $post_id ) );
     264
     265                $this->assertFalse( check_comment( 'Site Title', '', $source_url, 'Excerpt.', '192.168.0.1', '', $comment_type ) );
     266        }
     267
     268        /**
     269         * Manually approving every comment must not be bypassed by a self-pingback.
     270         *
     271         * @ticket 65016
     272         */
     273        public function test_should_return_false_for_a_pingback_from_this_site_when_comment_moderation_is_enabled() {
     274                update_option( 'comment_moderation', '1' );
     275
     276                $source_url = get_permalink( self::factory()->post->create() );
     277
     278                $this->assertFalse( check_comment( 'Site Title', '', $source_url, 'Excerpt.', '192.168.0.1', '', 'pingback' ) );
     279        }
     280
     281        /**
     282         * The source post must be published, not a draft that happens to resolve.
     283         *
     284         * @ticket 65016
     285         */
     286        public function test_should_return_false_for_a_pingback_from_an_unpublished_post_on_this_site() {
     287                update_option( 'comment_previously_approved', '1' );
     288
     289                $post_id    = self::factory()->post->create( array( 'post_status' => 'draft' ) );
     290                $source_url = add_query_arg( 'p', $post_id, home_url( '/' ) );
     291
     292                $this->assertFalse( check_comment( 'Site Title', '', $source_url, 'Excerpt.', '192.168.0.1', '', 'pingback' ) );
     293        }
     294
     295        /**
     296         * Only pings are exempt. A regular comment linking to a local post is still moderated.
     297         *
     298         * @ticket 65016
     299         */
     300        public function test_should_return_false_for_a_comment_whose_author_url_is_a_post_on_this_site() {
     301                update_option( 'comment_previously_approved', '1' );
     302
     303                $source_url = get_permalink( self::factory()->post->create() );
     304
     305                $this->assertFalse( check_comment( 'Bob', 'bob@example.com', $source_url, 'A comment.', '192.168.0.1', '', 'comment' ) );
     306        }
     307
     308        /**
     309         * @ticket 65016
     310         */
     311        public function test_auto_approve_pingback_should_be_able_to_hold_a_pingback_from_this_site() {
     312                update_option( 'comment_previously_approved', '1' );
     313
     314                $source_url = get_permalink( self::factory()->post->create() );
     315
     316                add_filter( 'auto_approve_pingback', '__return_false' );
     317
     318                $this->assertFalse( check_comment( 'Site Title', '', $source_url, 'Excerpt.', '192.168.0.1', '', 'pingback' ) );
     319        }
     320
     321        /**
     322         * @ticket 65016
     323         */
     324        public function test_auto_approve_pingback_should_be_able_to_approve_a_pingback_from_another_site() {
     325                update_option( 'comment_previously_approved', '1' );
     326
     327                add_filter( 'auto_approve_pingback', '__return_true' );
     328
     329                $this->assertTrue( check_comment( 'Site Title', '', 'http://example.com/a-post/', 'Excerpt.', '192.168.0.1', '', 'pingback' ) );
     330        }
     331
     332        /**
     333         * @ticket 65016
     334         */
     335        public function test_auto_approve_pingback_should_receive_the_source_post_id() {
     336                update_option( 'comment_previously_approved', '1' );
     337
     338                $post_id    = self::factory()->post->create();
     339                $source_url = get_permalink( $post_id );
     340
     341                $observed = null;
     342                add_filter(
     343                        'auto_approve_pingback',
     344                        static function ( $approve, $source_id ) use ( &$observed ) {
     345                                $observed = $source_id;
     346                                return $approve;
     347                        },
     348                        10,
     349                        2
     350                );
     351
     352                check_comment( 'Site Title', '', $source_url, 'Excerpt.', '192.168.0.1', '', 'pingback' );
     353
     354                $this->assertSame( $post_id, $observed );
     355        }
     356
     357        /**
     358         * Data provider.
     359         *
     360         * @return array[]
     361         */
     362        public function data_ping_types() {
     363                return array(
     364                        'pingback'  => array( 'pingback' ),
     365                        'trackback' => array( 'trackback' ),
     366                );
     367        }
    208368}
  • trunk/tests/phpunit/tests/comment/wpAllowComment.php

    r53863 r63036  
    7272                $result = wp_allow_comment( $comment_data );
    7373        }
     74
     75        /**
     76         * @ticket 65016
     77         *
     78         * @dataProvider data_should_approve_a_pingback_only_when_it_comes_from_this_site
     79         *
     80         * @param bool $is_self_ping Whether the pingback should come from this site.
     81         * @param int  $expected     The expected approval status.
     82         */
     83        public function test_should_approve_a_pingback_only_when_it_comes_from_this_site( $is_self_ping, $expected ) {
     84                update_option( 'comment_previously_approved', '1' );
     85
     86                $source_url = $is_self_ping
     87                        ? get_permalink( self::factory()->post->create() )
     88                        : 'http://example.com/their-post/';
     89
     90                $comment_data = array(
     91                        'comment_post_ID'      => self::$post_id,
     92                        'comment_author'       => 'The Linking Post',
     93                        'comment_author_email' => '',
     94                        'comment_author_url'   => $source_url,
     95                        'comment_content'      => '[…] an earlier post of mine […]',
     96                        'comment_author_IP'    => '192.168.0.1',
     97                        'comment_parent'       => 0,
     98                        'comment_date_gmt'     => gmdate( 'Y-m-d H:i:s' ),
     99                        'comment_agent'        => 'WordPress/6.8',
     100                        'comment_type'         => 'pingback',
     101                );
     102
     103                $this->assertSame( $expected, wp_allow_comment( $comment_data ) );
     104        }
     105
     106        /**
     107         * Data provider.
     108         *
     109         * @return array[]
     110         */
     111        public function data_should_approve_a_pingback_only_when_it_comes_from_this_site() {
     112                return array(
     113                        'a pingback from this site'    => array( true, 1 ),
     114                        'a pingback from another site' => array( false, 0 ),
     115                );
     116        }
    74117}
Note: See TracChangeset for help on using the changeset viewer.