Skip to content

Security: Require a nonce-protected POST to delete a custom role (GO-54) - #2942

Merged
corsacca merged 1 commit into
developfrom
GO-54-role-delete-nonce
Oct 23, 2026
Merged

corsacca merged 1 commit into
developfrom
GO-54-role-delete-nonce

Conversation

@corsacca

Copy link
Copy Markdown
Member

Summary

Deleting a custom role (Settings (D.T) → Roles) was a state-changing GET: once confirm was in the query string, delete_action() removed the role from dt_custom_roles with only a delete_roles capability check. An admin who opened a link or loaded an image pointing at admin.php?page=dt_options&tab=roles&action=delete&role=<slug>&confirm=1 would delete that role, and every user holding it would lose its capabilities.

  • The confirm view's Delete Role button is now a POST form carrying a role_delete_<slug> nonce (wp_nonce_field).
  • delete_action() deletes only when that nonce verifies for the role in the URL. A GET (including confirm=1) shows the confirm view. A missing or wrong nonce, including one minted for a different role, shows "Unauthorized" plus the confirm view.

Internal ref: GO-54.

Verification

  • php -l and vendor/bin/phpcs pass on the changed file.
  • Live test on a local multisite as an administrator, with a temporary custom role:
    • Before the fix, GET …&action=delete&role=custom_go54_test&confirm=1 deleted the role.
    • After the fix, the same GET shows the confirm page and the role remains.
    • POST with a forged nonce returns Unauthorized and the role remains.
    • POST with a nonce from another role's confirm page returns Unauthorized and the role remains.
    • Clicking Delete Role in the browser deletes the role and returns to the roles list. The button looks the same as before.

🤖 Generated with Claude Code

The delete confirm step was a GET link, so any page could delete a custom
role by sending an admin to ?action=delete&role=<slug>&confirm=1. The
confirm view now posts a form carrying a role_delete_<slug> nonce, and the
handler only deletes after verifying it; a GET or a bad nonce shows the
confirm view.
@github-actions

Copy link
Copy Markdown

Review

High: none.

Medium: none.

I traced delete_action(), delete() and view_delete() in tab-custom-roles.php. The change is correct:

  • The delete confirmation is now a POST form with a nonce tied to the role slug (role_delete_<slug>). That closes the CSRF hole in the old &confirm=true GET link.
  • The capability check still runs first. A missing or invalid nonce re-renders the confirmation view with an error. delete() only reads $_GET['role'], which stays in the form action URL.
  • confirm is no longer referenced anywhere else.
  • sanitize_key on the nonce is safe because WordPress nonces are lowercase hex.

I did not run the code or tests; this is from reading the diff and surrounding code.

The PR looks ready to merge.

@corsacca
corsacca merged commit 635d29b into develop Oct 23, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant