fix: enforce per-post edit_post capability in bulk audio handlers#572
Merged
Conversation
The redirect-based bulk actions handle_bulk_generate_action() and
handle_bulk_delete_action() acted on the selected post IDs without a
per-post capability check. WordPress core routes custom bulk actions
through the handle_bulk_actions-edit-{post_type} filter after only a
coarse current_user_can(edit_posts) gate, so a Contributor/Author could
craft an edit.php request targeting another author's post to force
(billable) audio generation, or remotely delete audio and wipe all
BeyondWords/speechkit meta, on content they cannot edit.
Filter $object_ids by current_user_can('edit_post', $id) at the top of
both handlers, mirroring the guard already present in the AJAX sibling
save_bulk_edit(), and bail with a zero count when nothing editable
remains (so the delete path never hands an empty batch to the API).
Add PHPUnit coverage for the per-post filtering on both redirect
handlers, and fix two existing integration tests that invoked the
handlers as user 0 (now correctly rejected by the gate) to run as an
administrator.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
✅ WordPress Plugin Check Report
📊 ReportAll checks passed! No errors or warnings found. 🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check |
galbus
marked this pull request as ready for review
July 19, 2026 07:29
gouravkhunger
approved these changes
Jul 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The redirect-based bulk actions in
src/posts-list/class-bulk-edit.phpmutated and remotely deleted BeyondWords audio without a per-post capability check, allowing a low-privilege user to act on posts they cannot edit.handle_bulk_generate_action()andhandle_bulk_delete_action()are wired to thehandle_bulk_actions-edit-{post_type}filter. WordPress core routes custom bulk actions through that filter after onlycheck_admin_referer('bulk-posts')and the coarsecurrent_user_can($ptype->cap->edit_posts)gate at the top ofedit.php— it never checks per-postedit_postfor custom actions (only for built-ins like trash). Both handlers then acted on every submitted ID.The AJAX sibling
save_bulk_edit()in the same class already filters IDs bycurrent_user_can('edit_post', $id)(with a comment explaining exactly this risk); the redirect handlers omitted the equivalent guard.Impact
A Contributor or Author (both hold
edit_posts, so they can loadedit.phpand read thebulk-postsnonce from their own list-table form) could craft:Core
intval()s the IDs and passes them straight to the filter. The result was a privilege-boundary violation on content the user cannot edit:beyondwords_*/speechkit_*meta keys per post (irreversible).beyondwords_generate_audiometa writes + billable API generation/regeneration.The list-table UI hides checkboxes for uneditable posts, but the crafted request bypasses that.
Fix
At the top of both handlers, filter the IDs exactly like the AJAX path:
…and bail with a zero-count redirect when nothing editable remains. On the delete path this also prevents handing an empty batch to the API (which would otherwise surface a spurious
BULK-NO-RESPONSEerror).Tests
tests/phpunit/posts-list/test-bulk-edit.php:wp_set_current_user(0)reset intearDown.Verification
npm run phpcs(WordPress-VIP-Go): clean, nophpcs:ignoreadded.--filter BulkEdit: 27 tests, 110 assertions, all pass (4 new tests, 2 fixed integration tests, and all existing AJAX-path coverage). Full Cypress suite not run.Reviewer notes
check_admin_referer('bulk-posts'), so no nonce change was needed here.