Description
Notes @ mentions are currently rendered as anchors carrying the mentioned user in a pair of CSS classes:
<a class="wp-note-mention user-12" href="…">@Name</a>
That markup causes two separate problems.
1. Mentions are not links, but the Link format claims them
A mention is a token, not a navigable link. Because it is an a element, the Link format UI attaches to it: placing the caret inside a saved mention opens the link popover, and editing through it destroys the mention markup (screencast). The user ID is silently lost and the chip degrades to a plain link.
2. The kses allowance that lets mentions survive a save is open-ended
Comment content is filtered through kses in the pre_comment_content context, whose minimal $allowedtags allow only href/title on a. To let mention markup survive a save, #80221 added an allowance for class on a - but class is open-ended, so it cannot be left on permanently for every comment. It is instead armed and disarmed around each note write:
preprocess_comment / rest_preprocess_comment arm the allowance when the comment being written is a note,
- a self-removing
pre_comment_content filter at PHP_INT_MAX disarms it,
- a
rest_request_after_callbacks backstop disarms it if the write bails out before that point.
Two consequences:
- Blast radius. While armed, a note author without
unfiltered_html can persist any class on a link, not just user-N. Arbitrary classes on admin-rendered content are a live CSS and JavaScript selector surface. This was @westonruter's original concern on the Core backport.
- Fragility. Correctness depends on every arm having a matching disarm across two request paths (direct and REST) - roughly 120 lines of stateful machinery in
lib/compat/wordpress-7.1/block-comments.php guarding a single integer.
Proposed change
Render the mention as a non-interactive span chip and narrow the allowance instead of arming it:
-<a class="wp-note-mention user-12" href="…">@Name</a>
+<span class="wp-note-mention user-12">@Name</span>
- A
span takes mentions out of the Link format UI entirely, fixing problem 1. Keeping the wp-note-mention class prevents any registered format from claiming the element (formats are differentiated by tag name and class), and RichText round-trips the unregistered span without a dedicated mention format.
- Replace the arm/disarm machinery with two always-on filters: one allowing
span with class in pre_comment_content, and one running immediately after wp_filter_kses that reduces every span's class to the only two tokens the mention markup uses (wp-note-mention and user-N). The allowance becomes unconditional but is no longer open-ended, so there is nothing to arm and disarm.
The narrowing must only apply while the restrictive comment allowlist is active - users with unfiltered_html are filtered through wp_filter_post_kses, where arbitrary classes are already legal, and narrowing their markup would restrict what core permits them to post.
Accepted behavior change: any commenter can then persist <span> elements whose class is limited to those two tokens. This is inert - span is semantics-free, neither token is a styling or scripting hook outside the notes sidebar, and mention notification parsing only processes note-type comments.
Note on using a data attribute instead
The first attempt carried the user ID in data-wp-note-mention-user on the anchor. That was dropped for two reasons:
- It does not avoid the kses allowance.
data-* attributes are default-allowed in the post context only; the comment context uses the minimal $allowedtags, and the data-* wildcard path in wp_kses_attr_check() only applies when the allowlist already contains data-*. So an explicit allowance is required either way, and a strictly-allowlisted class has no less security value than an inert attribute.
- An attribute unregistered with the link format was stripped by the format on edit, so it did not survive problem 1 either.
Legacy markup
The wp-note-mention / user-N classes have never shipped in a stable WordPress release, so no legacy selector or fallback needs to be retained for the tag-name change.
Step-by-step reproduction instructions
Problem 1:
- As a user without the
unfiltered_html capability (e.g. an author), open a post, add a note on a block, type @ and pick a teammate, then save the note.
- Place the caret inside the saved mention chip. The Link format popover appears.
- Edit the mention through the link popover - the mention markup is destroyed and the user ID is lost.
Problem 2:
- With the same user, save a note containing a mention.
- Inspect the saved anchor: it carries
class="wp-note-mention user-N", persisted through the temporarily-armed kses class allowance, which during that window would equally have persisted any other class.
Follow-ups
- #79606 (mention notifications) parses mention anchors in PHP and needs to match mention spans instead; the
user-N class parsing itself is unchanged. It should also gain an identity check before notifying, since the chip text can be partially edited while the span retains its user-N class.
- wordpress-develop#12503 reduces to the same two filters in Core.
Environment info
- Gutenberg
trunk
- Affects
lib/compat/wordpress-7.1/block-comments.php and the collab sidebar mention completer
Description
Notes
@mentions are currently rendered as anchors carrying the mentioned user in a pair of CSS classes:That markup causes two separate problems.
1. Mentions are not links, but the Link format claims them
A mention is a token, not a navigable link. Because it is an
aelement, the Link format UI attaches to it: placing the caret inside a saved mention opens the link popover, and editing through it destroys the mention markup (screencast). The user ID is silently lost and the chip degrades to a plain link.2. The kses allowance that lets mentions survive a save is open-ended
Comment content is filtered through kses in the
pre_comment_contentcontext, whose minimal$allowedtagsallow onlyhref/titleona. To let mention markup survive a save, #80221 added an allowance forclassona- butclassis open-ended, so it cannot be left on permanently for every comment. It is instead armed and disarmed around each note write:preprocess_comment/rest_preprocess_commentarm the allowance when the comment being written is a note,pre_comment_contentfilter atPHP_INT_MAXdisarms it,rest_request_after_callbacksbackstop disarms it if the write bails out before that point.Two consequences:
unfiltered_htmlcan persist any class on a link, not justuser-N. Arbitrary classes on admin-rendered content are a live CSS and JavaScript selector surface. This was @westonruter's original concern on the Core backport.lib/compat/wordpress-7.1/block-comments.phpguarding a single integer.Proposed change
Render the mention as a non-interactive
spanchip and narrow the allowance instead of arming it:spantakes mentions out of the Link format UI entirely, fixing problem 1. Keeping thewp-note-mentionclass prevents any registered format from claiming the element (formats are differentiated by tag name and class), and RichText round-trips the unregistered span without a dedicated mention format.spanwithclassinpre_comment_content, and one running immediately afterwp_filter_ksesthat reduces every span'sclassto the only two tokens the mention markup uses (wp-note-mentionanduser-N). The allowance becomes unconditional but is no longer open-ended, so there is nothing to arm and disarm.The narrowing must only apply while the restrictive comment allowlist is active - users with
unfiltered_htmlare filtered throughwp_filter_post_kses, where arbitrary classes are already legal, and narrowing their markup would restrict what core permits them to post.Accepted behavior change: any commenter can then persist
<span>elements whose class is limited to those two tokens. This is inert -spanis semantics-free, neither token is a styling or scripting hook outside the notes sidebar, and mention notification parsing only processesnote-type comments.Note on using a data attribute instead
The first attempt carried the user ID in
data-wp-note-mention-useron the anchor. That was dropped for two reasons:data-*attributes are default-allowed in the post context only; the comment context uses the minimal$allowedtags, and thedata-*wildcard path inwp_kses_attr_check()only applies when the allowlist already containsdata-*. So an explicit allowance is required either way, and a strictly-allowlisted class has no less security value than an inert attribute.Legacy markup
The
wp-note-mention/user-Nclasses have never shipped in a stable WordPress release, so no legacy selector or fallback needs to be retained for the tag-name change.Step-by-step reproduction instructions
Problem 1:
unfiltered_htmlcapability (e.g. an author), open a post, add a note on a block, type@and pick a teammate, then save the note.Problem 2:
class="wp-note-mention user-N", persisted through the temporarily-armed ksesclassallowance, which during that window would equally have persisted any other class.Follow-ups
user-Nclass parsing itself is unchanged. It should also gain an identity check before notifying, since the chip text can be partially edited while the span retains itsuser-Nclass.Environment info
trunklib/compat/wordpress-7.1/block-comments.phpand the collab sidebar mention completer