Normal view

When capability checks aren't the whole protection boundary in WordPress

27 August 2026 at 00:15

I've been working on protections around destructive WordPress operations, and recent compatibility testing made me rethink an assumption I originally considered fairly safe.

My starting point was simple:

If I restrict a protected user's relevant WordPress capability, the operation should not happen.

For example, I denied `activate_plugins` as part of the protection and tested plugin activation.

That worked as expected through the normal WordPress admin workflow: the user could no longer activate a plugin from the admin interface.

I then tested the same operation through an automated WordPress tool.

The tool had its own permission check before execution:

`current_user_can( 'activate_plugins' )`

But the underlying operation was then performed by calling `activate_plugin()` directly.

Despite the capability-based protection blocking the normal admin path, the operation could still reach the underlying activation logic through this different execution path.

That exposed an important distinction.

The normal admin flow and the underlying operation are not necessarily the same protection boundary. A function such as `activate_plugin()` can assume that authorization has already been handled by its caller.

From the perspective of the calling tool, that may be perfectly valid.

But from the perspective of a site-level protection designed to prevent the plugin state from changing, relying only on capability removal was not enough for every execution path we tested.

I found that observing and enforcing the actual state transition through `pre_update_option_active_plugins` allowed the protection to stop the activation regardless of that particular caller path.

The same line of testing has led us into plugin installation and other destructive administrative operations as well. Different operations have different hooks and execution paths, so I don't think there is one universal answer.

I'm not claiming that WordPress capabilities are broken, or that every underlying WordPress function should perform its own permission check. The important distinction is that the capability check and the underlying operation are separate layers, and reaching the operation directly can produce a different protection outcome.

The question that came out of this testing is different:

If you are building a site-level guardrail whose purpose is to prevent a particular destructive state change, should the protection depend entirely on every possible caller enforcing authorization before reaching that operation?

Or should some protections be enforced closer to the operation or state transition itself?

As more automated and AI-driven tools perform WordPress administration, I suspect this distinction will become more important.

I'd be genuinely interested to hear how other WordPress developers think about the boundary between authorization and operational protection.

submitted by /u/plugiva
[link] [comments]
❌