Skip to:
Content

bbPress.org

Changeset 7847


Ignore:
Timestamp:
10/06/2026 09:55:16 AM (11 hours ago)
Author:
johnjamesjacoby
Message:

Security: Prevent shortcodes in forum content.

Prevent WordPress content loops from executing shortcodes stored in forum, topic, reply, and extension-defined bbPress post content. Preserve shortcodes and bbPress blocks in ordinary Pages, classic Page Templates, and forum and topic root Pages, with a filterable allow-list for intentional integrations.

In trunk, for 2.7.

Location:
trunk
Files:
1 added
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/CHANGELOG.md

    r7845 r7847  
    3939### Security
    4040
     41- Kept bbPress post shortcodes literal in WordPress content loops.
    4142- Strengthened password checks for topic and reply submissions.
    4243- Limited block editor topic-tag choices to public discussions in accessible forums.
  • trunk/src/includes/common/formatting.php

    r7652 r7847  
    1010// Exit if accessed directly
    1111defined( 'ABSPATH' ) || exit;
     12
     13/** WordPress Content *********************************************************/
     14
     15/**
     16 * Filter shortcodes in bbPress post content in WordPress content loops.
     17 *
     18 * bbPress renders its post content without running shortcodes, but a theme can
     19 * pass the raw content through WordPress's the_content filter. Offer a bbPress
     20 * sub-filter for this case without affecting shortcodes in other post types.
     21 *
     22 * @since 2.6.20 bbPress (r7847)
     23 *
     24 * @param false|string $output  Short-circuit value from an earlier filter.
     25 * @param string       $tag     Shortcode name.
     26 * @param array        $attr    Shortcode attributes.
     27 * @param array        $matches Shortcode regular expression match.
     28 * @return false|string Filtered short-circuit value.
     29 */
     30function bbp_pre_do_shortcode_tag( $output, $tag, $attr, $matches ) {
     31        if ( ! doing_filter( 'the_content' ) ) {
     32                return $output;
     33        }
     34
     35        if ( ! in_array( get_post_type(), bbp_get_post_types(), true ) ) {
     36                return $output;
     37        }
     38
     39        return apply_filters( 'bbp_pre_do_shortcode_tag', $output, $tag, $attr, $matches );
     40}
     41
     42/**
     43 * Prevent shortcodes in bbPress post content unless explicitly allowed.
     44 *
     45 * The allow-list is empty by default because bbPress does not execute
     46 * shortcodes in forum, topic, or reply content on its own content paths.
     47 *
     48 * @since 2.6.20 bbPress (r7847)
     49 *
     50 * @param false|string $output  Short-circuit value from an earlier filter.
     51 * @param string       $tag     Shortcode name.
     52 * @param array        $attr    Shortcode attributes.
     53 * @param array        $matches Shortcode regular expression match.
     54 * @return false|string Short-circuit value or the literal shortcode.
     55 */
     56function bbp_prevent_content_shortcodes( $output, $tag, $attr, $matches ) {
     57        /**
     58         * Filter shortcode tags allowed to run in bbPress post content.
     59         *
     60         * @since 2.6.20 bbPress (r7847)
     61         *
     62         * @param array        $allowed   Allowed shortcode tags. Empty by default.
     63         * @param string|false $post_type Current post type.
     64         */
     65        $allowed = (array) apply_filters( 'bbp_allowed_content_shortcodes', array(), get_post_type() );
     66
     67        if ( ! in_array( $tag, $allowed, true ) ) {
     68                $output = $matches[0];
     69        }
     70
     71        return $output;
     72}
    1273
    1374/** Kses **********************************************************************/
  • trunk/src/includes/core/filters.php

    r7709 r7847  
    4949add_filter( 'logout_url',              'bbp_logout_url',         2,  2 );
    5050add_filter( 'plugin_locale',           'bbp_plugin_locale',      10, 2 );
     51
     52// Apply bbPress shortcode policy to forum, topic, and reply content.
     53add_filter( 'pre_do_shortcode_tag',     'bbp_pre_do_shortcode_tag', 10, 4 );
     54add_filter( 'bbp_pre_do_shortcode_tag', 'bbp_prevent_content_shortcodes', 10, 4 );
    5155
    5256// Keep WordPress author discovery limited to non-forum posts.
  • trunk/src/includes/core/theme-compat.php

    r7653 r7847  
    478478
    479479/**
     480 * Filter the content of a forum or topic archive root page.
     481 *
     482 * Root pages are ordinary WordPress pages, even though an archive post may be
     483 * current while their content is filtered. Temporarily use the root page as
     484 * the current query post so shortcodes restore the correct post data.
     485 *
     486 * @since 2.6.20 bbPress (r7847)
     487 *
     488 * @param WP_Post $page Root page.
     489 * @return string Filtered page content.
     490 */
     491function bbp_get_theme_compat_page_content( $page ) {
     492        global $post;
     493
     494        $wp_query = bbp_get_wp_query();
     495        $content  = ( $page instanceof WP_Post ) ? $page->post_content : '';
     496
     497        // Fall back to the previous behavior without a valid post and main query.
     498        if ( ! ( $page instanceof WP_Post ) || ! ( $wp_query instanceof WP_Query ) ) {
     499                return apply_filters( 'the_content', $content );
     500        }
     501
     502        // The main query post is also the current global post in this call path.
     503        $original_query_post = $wp_query->post;
     504
     505        try {
     506                // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
     507                $post           = $page;
     508                $wp_query->post = $page;
     509                $wp_query->setup_postdata( $page );
     510
     511                return apply_filters( 'the_content', $content );
     512        } finally {
     513                $wp_query->post = $original_query_post;
     514                wp_reset_postdata();
     515        }
     516}
     517
     518/**
    480519 * Reset main query vars and filter 'the_content' to output a bbPress
    481520 * template part as needed.
    … …  
    562601                // ...or use the existing page content?
    563602                } else {
    564                         $new_content = apply_filters( 'the_content', $page->post_content );
     603                        $new_content = bbp_get_theme_compat_page_content( $page );
    565604                }
    566605
    … …  
    649688                // ...or use the existing page content?
    650689                } else {
    651                         $new_content = apply_filters( 'the_content', $page->post_content );
     690                        $new_content = bbp_get_theme_compat_page_content( $page );
    652691                }
    653692
  • trunk/tests/phpunit/testcases/core/theme-compat.php

    r7424 r7847  
    7777
    7878        /**
     79         * A root page's shortcode must run even when a bbPress post is ambient.
     80         *
     81         * @dataProvider get_root_page_content_cases
     82         *
     83         * @param string $post_type      Ambient bbPress post type.
     84         * @param string $archive_filter Archive condition to enable.
     85         */
     86        public function test_root_page_content_runs_shortcodes( $post_type, $archive_filter ) {
     87                $old_permalinks  = get_option( 'permalink_structure' );
     88                $old_post        = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : null;
     89                $old_query_post  = isset( $GLOBALS['wp_query']->post ) ? $GLOBALS['wp_query']->post : null;
     90                $old_query_posts = isset( $GLOBALS['wp_query']->posts ) ? $GLOBALS['wp_query']->posts : array();
     91                $root_slug       = ( bbp_get_forum_post_type() === $post_type ) ? bbp_get_root_slug() : bbp_get_topic_archive_slug();
     92                $page_id         = $this->factory->post->create(
     93                        array(
     94                                'post_type'    => 'page',
     95                                'post_name'    => $root_slug,
     96                                'post_content' => '[bbp-stats] [bbp_content_probe]',
     97                        )
     98                );
     99                $ambient_id      = $this->factory->post->create( array( 'post_type' => $post_type ) );
     100                $render          = function( $output ) {
     101                        return $output . 'root-page-bbpress-ran';
     102                };
     103
     104                update_option( 'permalink_structure', '/%postname%/' );
     105                add_filter( $archive_filter, '__return_true' );
     106                add_filter( 'bbp_display_shortcode', $render );
     107                add_shortcode( 'bbp_content_probe', function() {
     108                        return sprintf( 'root-page-shortcode-ran-%d-%d', get_the_ID(), $GLOBALS['id'] );
     109                } );
     110
     111                try {
     112                        $GLOBALS['post'] = get_post( $ambient_id );
     113                        $GLOBALS['wp_query']->post  = $GLOBALS['post'];
     114                        $GLOBALS['wp_query']->posts = array( $GLOBALS['post'] );
     115                        $GLOBALS['wp_query']->setup_postdata( $GLOBALS['post'] );
     116                        $content = bbp_get_theme_compat_page_content( get_post( $page_id ) );
     117
     118                        $this->assertSame( $ambient_id, $GLOBALS['post']->ID );
     119                        $this->assertSame( $ambient_id, $GLOBALS['wp_query']->post->ID );
     120                        $this->assertSame( $ambient_id, $GLOBALS['id'] );
     121                        $this->assertStringContainsString( 'root-page-bbpress-ran', $content );
     122                        $this->assertStringContainsString( "root-page-shortcode-ran-{$page_id}-{$page_id}", $content );
     123
     124                        bbp_template_include_theme_compat( '/original-template.php' );
     125
     126                        $this->assertSame( $page_id, $GLOBALS['post']->ID );
     127                        $this->assertStringContainsString( "root-page-shortcode-ran-{$page_id}-{$page_id}", $GLOBALS['post']->post_content );
     128                } finally {
     129                        remove_shortcode( 'bbp_content_probe' );
     130                        remove_filter( 'bbp_display_shortcode', $render );
     131                        remove_filter( $archive_filter, '__return_true' );
     132                        update_option( 'permalink_structure', $old_permalinks );
     133                        $GLOBALS['post'] = $old_post;
     134                        $GLOBALS['wp_query']->post  = $old_query_post;
     135                        $GLOBALS['wp_query']->posts = $old_query_posts;
     136                }
     137        }
     138
     139        /**
     140         * Root page cases for forum and topic archives.
     141         *
     142         * @return array[] Root page cases.
     143         */
     144        public function get_root_page_content_cases() {
     145                return array(
     146                        'forum archive' => array( bbp_get_forum_post_type(), 'bbp_is_forum_archive' ),
     147                        'topic archive' => array( bbp_get_topic_post_type(), 'bbp_is_topic_archive' ),
     148                );
     149        }
     150
     151        /**
     152         * A root page's bbPress block and following shortcode must both run.
     153         *
     154         * @dataProvider get_root_page_content_cases
     155         *
     156         * @param string $post_type      Ambient bbPress post type.
     157         * @param string $archive_filter Archive condition to enable.
     158         */
     159        public function test_root_page_content_runs_bbpress_blocks( $post_type, $archive_filter ) {
     160                $old_permalinks  = get_option( 'permalink_structure' );
     161                $old_post        = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : null;
     162                $old_query_post  = isset( $GLOBALS['wp_query']->post ) ? $GLOBALS['wp_query']->post : null;
     163                $old_query_posts = isset( $GLOBALS['wp_query']->posts ) ? $GLOBALS['wp_query']->posts : array();
     164                $root_slug       = ( bbp_get_forum_post_type() === $post_type ) ? bbp_get_root_slug() : bbp_get_topic_archive_slug();
     165                $page_id         = $this->factory->post->create(
     166                        array(
     167                                'post_type'    => 'page',
     168                                'post_name'    => $root_slug,
     169                                'post_content' => '<!-- wp:bbpress/stats /--> [bbp_content_probe]',
     170                        )
     171                );
     172                $ambient_id      = $this->factory->post->create( array( 'post_type' => $post_type ) );
     173                $render          = function( $output ) {
     174                        return $output . 'root-block-bbpress-ran';
     175                };
     176
     177                update_option( 'permalink_structure', '/%postname%/' );
     178                add_filter( $archive_filter, '__return_true' );
     179                add_filter( 'bbp_display_shortcode', $render );
     180                add_shortcode( 'bbp_content_probe', function() {
     181                        return sprintf( 'root-block-shortcode-ran-%d-%d', get_the_ID(), $GLOBALS['id'] );
     182                } );
     183
     184                try {
     185                        $GLOBALS['post'] = get_post( $ambient_id );
     186                        $GLOBALS['wp_query']->post  = $GLOBALS['post'];
     187                        $GLOBALS['wp_query']->posts = array( $GLOBALS['post'] );
     188                        $GLOBALS['wp_query']->setup_postdata( $GLOBALS['post'] );
     189                        $content = bbp_get_theme_compat_page_content( get_post( $page_id ) );
     190
     191                        $this->assertSame( $ambient_id, $GLOBALS['post']->ID );
     192                        $this->assertSame( $ambient_id, $GLOBALS['wp_query']->post->ID );
     193                        $this->assertSame( $ambient_id, $GLOBALS['id'] );
     194                        $this->assertStringContainsString( 'root-block-bbpress-ran', $content );
     195                        $this->assertStringContainsString( "root-block-shortcode-ran-{$page_id}-{$page_id}", $content );
     196                } finally {
     197                        remove_shortcode( 'bbp_content_probe' );
     198                        remove_filter( 'bbp_display_shortcode', $render );
     199                        remove_filter( $archive_filter, '__return_true' );
     200                        update_option( 'permalink_structure', $old_permalinks );
     201                        $GLOBALS['post'] = $old_post;
     202                        $GLOBALS['wp_query']->post  = $old_query_post;
     203                        $GLOBALS['wp_query']->posts = $old_query_posts;
     204                }
     205        }
     206
     207        /**
     208         * Root page filtering must restore post data when a content filter throws.
     209         */
     210        public function test_root_page_content_restores_post_data_after_exception() {
     211                $page_id    = $this->factory->post->create( array( 'post_type' => 'page' ) );
     212                $ambient_id = $this->factory->forum->create();
     213                $old_post   = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : null;
     214                $old_query  = $GLOBALS['wp_query']->post;
     215                $throw      = function() {
     216                        throw new RuntimeException( 'Stop filtering root page content.' );
     217                };
     218
     219                $GLOBALS['post']          = get_post( $ambient_id );
     220                $GLOBALS['wp_query']->post = $GLOBALS['post'];
     221                $GLOBALS['wp_query']->setup_postdata( $GLOBALS['post'] );
     222                add_filter( 'the_content', $throw, 1 );
     223
     224                try {
     225                        bbp_get_theme_compat_page_content( get_post( $page_id ) );
     226                        $this->fail( 'The content filter did not throw.' );
     227                } catch ( RuntimeException $exception ) {
     228                        $this->assertSame( 'Stop filtering root page content.', $exception->getMessage() );
     229                        $this->assertSame( $ambient_id, $GLOBALS['post']->ID );
     230                        $this->assertSame( $ambient_id, $GLOBALS['wp_query']->post->ID );
     231                        $this->assertSame( $ambient_id, $GLOBALS['id'] );
     232                } finally {
     233                        remove_filter( 'the_content', $throw, 1 );
     234                        $GLOBALS['post']          = $old_post;
     235                        $GLOBALS['wp_query']->post = $old_query;
     236                }
     237        }
     238
     239        /**
    79240         * Theme compatibility template selection cases.
    80241         *
Note: See TracChangeset for help on using the changeset viewer.