Opened 26 hours ago
Last modified 4 hours ago
#66110 reviewing defect (bug)
Block Hooks run during wp_trim_excerpt(), leaking hooked-block markup into excerpts
| Reported by: | MythThrazz | Owned by: | bernhard-reiter |
|---|---|---|---|
| Priority: | normal | Milestone: | 7.2 |
| Component: | Formatting | Version: | 6.8 |
| Severity: | normal | Keywords: | has-patch has-unit-tests |
| Cc: | Focuses: |
Description (last modified by )
When a post has no manual excerpt, wp_trim_excerpt() generates one from post content. Since [59523] (#61074), the_content carries a Block Hooks callback at priority 8, apply_block_hooks_to_content originally, replaced by apply_block_hooks_to_content_from_post_object in [59838] (#61074, #62716), both first released in 6.8.0:
// src/wp-includes/default-filters.php:201 add_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object', 8 ); // BEFORE do_blocks().
wp_trim_excerpt() already unhooks do_blocks (priority 9, [56560] / #58682) and wp_filter_content_tags (priority 12, [55850] / #56588) around its inner apply_filters( 'the_content' ) call, but the priority-8 Block Hooks callback was never unhooked, so the Block Hooks algorithm still runs during excerpt generation.
Why this is a bug
- Hooked-block markup leaks into excerpts as plain text.
apply_block_hooks_to_content_from_post_object()wraps delimiter-free content in a temporarycore/freeformblock and then in acore/post-contentwrapper block (blocks.php:1295-1336) specifically so thatfirst_child/last_childhooked blocks still have an anchor. The algorithm therefore runs and serializes hooked blocks into the content. They can never render, becausedo_blocksis unhooked, and whenwp_trim_words()runswp_strip_all_tags()on the result (formatting.php:4126), the<!-- -->block delimiters and HTML tags are stripped, so any markup a hooked block carries (e.g. via the publichooked_blockfilter, blocks.php:1049/1120) leaks into the excerpt as plain text.
Measured: a block hooked to core/post-content as last_child, given innerHTML of <p>LEAKED HOOKED TEXT</p> via the hooked_block filter, turns the excerpt of a post whose content is <p>Visible text.</p> from 'Visible text.' into 'Visible text. LEAKED HOOKED TEXT'.
- The work is guaranteed waste. Even when nothing leaks, the run can never have a visible effect, because
do_blocksis unhooked, so inserted hooked blocks are discarded. Every excerpt generation pays aparse_blocks()traversal,get_hooked_blocks(), thehooked_block_types/hooked_blockfilters, and a_wp_ignored_hooked_blockspost-meta read (blocks.php:1310) for nothing.
- Wrong context. The callback resolves its post context via
get_post()when none is passed (blocks.php:1280-1282), i.e. the current loop post, not the post passed towp_trim_excerpt(). When an excerpt is generated for a different post (widgets, REST, related-posts lists),_wp_ignored_hooked_blocksmeta is read from the wrong post and the wrong post's hooked-block suppression is applied.
Minimal reproduction
// Register a hooked block, e.g. in a plugin or theme functions.php. register_block_type( 'tests/hooked-block', array( 'block_hooks' => array( 'core/post-content' => 'last_child', ), ) ); // Give the hooked block markup via the public 'hooked_block' filter. add_filter( 'hooked_block', static function ( $parsed_hooked_block ) { $parsed_hooked_block['innerHTML'] = '<p>LEAKED HOOKED TEXT</p>'; $parsed_hooked_block['innerContent'] = array( '<p>LEAKED HOOKED TEXT</p>' ); return $parsed_hooked_block; } );
Then, on the front end (inside the loop so get_post() resolves), for a post whose content is <!-- wp:paragraph --><p>Visible text.</p><!-- /wp:paragraph --> and which has no manual excerpt:
$excerpt = wp_trim_excerpt( '', $post ); // or the_excerpt() in the loop
Expected: Visible text.
Actual: Visible text. LEAKED HOOKED TEXT
Precedent
Core already performs exactly this unhook for this exact callback: insert_hooked_blocks_into_rest_response() removes apply_block_hooks_to_content_from_post_object from the_content around its own inner apply_filters( 'the_content' ) call and restores it afterwards (blocks.php:1559-1573, the unhook/restore was introduced in [59523] for apply_block_hooks_to_content and switched to the current callback in [59838]). The REST API does it to avoid running the algorithm twice; the excerpt case is stronger, because the run can never produce rendered output at all.
Proposed fix
Mirror the existing do_blocks/wp_filter_content_tags dance in wp_trim_excerpt() (formatting.php:4045-4069): capture remove_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object', 8 ) before the inner apply_filters( 'the_content' ), and re-add it afterwards only if it was actually removed, the same conditional-restore pattern, so a site that deliberately unhooked the callback is not changed.
The patch includes six PHPUnit tests: the callback is unhooked inside the inner the_content run, is restored afterwards, is not resurrected when previously unhooked, the Block Hooks algorithm does not run (detected via the hooked_block_types filter, which only fires inside the algorithm, blocks.php:1025/1106), hooked-block markup does not leak into the excerpt, and Block Hooks still apply to a subsequent real the_content run.
Test sweep: 5283 tests pass across the formatting / post / query / REST-posts / feed groups; PHPCS clean. A negative control (the same reproduction on the code path without the fix) produces the leaked excerpt 'Visible text. LEAKED HOOKED TEXT', confirming the leak exists on trunk.
AI assistance: Yes.
Tool(s): Claude Code (orchestration/review) and Devin (implementation).
Used for: root-cause analysis, patch and test authorship; verified against git history (changesets, tickets, line numbers) and a full test sweep; reviewed and taken responsibility for by the contributor.
Change History (8)
This ticket was mentioned in PR #13524 on WordPress/wordpress-develop by @MythThrazz.
26 hours ago
#1
- Keywords has-patch has-unit-tests added
#2
@
23 hours ago
- Keywords changes-requested added
I'm seeing strange characters appearing in the description, like "â". Please correct this and remove the soft line breaks so tha tthe paragraphs reflow freely based on the screen size.
#3
@
23 hours ago
- Focuses performance removed
I don't believe this is related to performance. Please correct if otherwise.
#5
@
17 hours ago
- Description modified (diff)
Thanks. I fixed the encoding (the orphan characters were em dashes that got mangled on submit) and reflowed the description so the paragraphs wrap freely. Agreed on dropping the performance focus and the move to Formatting; the correctness bug, the hooked-block leak into excerpts and the wrong-post context, is the point here, and the saved work is incidental.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
## What / why
Since [59523] (#61074, WP 6.8),
the_contentcarries a Block Hooks callback at priority 8 —apply_block_hooks_to_content, swapped forapply_block_hooks_to_content_from_post_objectin [59838] (#61074, #62716).
wp_trim_excerpt()already unhooksdo_blocks([56560] / #58682)and
wp_filter_content_tags([55850] / #56588) around its innerapply_filters( 'the_content' )call, but the priority-8 callback was never unhooked. Three consequences:
core/post-contentwrapper block sofirst_child/last_childhooked blocks have an anchor; the algorithm serializes them into the content,do_blocksis unhooked so they can never render, andwp_strip_all_tags()strips thedelimiters — leaving the markup a hooked block carries via thehooked_blockfilter as literal excerpt text. Reproduced: excerpt goes from'Visible text.'to'Visible text. LEAKED HOOKED TEXT'.parse_blocks()traversal,get_hooked_blocks(), thehooked_block_types/hooked_blockfilters, and a_wp_ignored_hooked_blocksmeta read on every generated excerpt, for output that can never be visible.get_post()(current loop post), not the post passed towp_trim_excerpt(), so off-loop excerpts read the wrong post's_wp_ignored_hooked_blockssuppression.## The change
Unhook and conditionally restore
apply_block_hooks_to_content_from_post_object(priority 8)inside
wp_trim_excerpt(), mirroring the existingdo_blocks/wp_filter_content_tagsdance andthe identical unhook core already performs in
insert_hooked_blocks_into_rest_response()(blocks.php:1559-1573). A site that deliberately unhooked the callback is left unchanged.
## Testing
tests/phpunit/tests/formatting/wpTrimExcerpt.php: callback unhooked during the innerthe_contentrun; restored afterwards; not resurrected when previously unhooked; Block Hooks algorithm does not run (spied viahooked_block_types, which only fires inside the algorithm); hooked-block markup does not leak into the excerpt; Block Hooks still apply to a subsequent realthe_contentrun.'Visible text. LEAKED HOOKED TEXT', confirming the bug on trunk.## Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code (Anthropic), Devin (Cognition)
Model(s): Claude Opus 4.8 (orchestration/review), Devin (implementation)
Used for: root-cause analysis, patch + test authorship, git fact-checking, and full test sweep; reviewed and taken responsibility for by the contributor.
---
This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.
🤖 Generated with Claude Code