multisite-security
Skill45 starsChanged 54 days agoMITView on GitHub
Use when writing code that runs on a WordPress multisite network - switch_to_blog, network admin pages, get_sites, or capabilities that differ between site and network scope. Uses manage_network / manage_network_options and is_super_admin correctly, restores context with restore_current_blog, isolates per-site data, and never trusts a blog id from input. Prevents cross-site data leakage and network privilege escalation.
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 3 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 20
Use this skill whenever code runs on a WordPress multisite network:
- Distinctive: line 48
Keep per-site data isolated: Do not store site A's data in site B's options or tables unless the user explicitly authorized cross-site …
- Very specific: line 50
is_super_admin()is for network-level checks only: Prefer current_user_can( 'manage_network' ) for capabilities. - Very specific: line 55
Check is_multisite() before running multisite-specific code.
- Distinctive: line 63
After switching, re-check capabilities if the action is privileged.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Multisite security
When to use this skill
Use this skill whenever code runs on a WordPress multisite network:
- Using
switch_to_blog()to access another site's data. - Adding network admin pages (
network_admin_menu). - Enumerating sites with
get_sites(). - Reading a
blog_idfrom a request parameter or URL. - Performing actions that affect the whole network vs. a single site.
On multisite, site administrator and network administrator are different roles.
Network-level actions require manage_network or manage_network_options, and
switch_to_blog() must always be paired with restore_current_blog().
Related: see the capability-permission-checks skill for single-site capability
patterns.
Core principles (and why they matter)
- Site admin != network admin. A site administrator can do almost anything on their
own site, but they must not manage network settings or access other sites' data.
Network actions need
manage_network/manage_network_options. - Always pair
switch_to_blog()withrestore_current_blog(). Forgetting to restore leaves the rest of the request running in the wrong site context, leaking or corrupting data. - Re-check capabilities after switching. Capabilities are evaluated in the current
site context. After
switch_to_blog(), the capability context changes; re-verifycurrent_user_can()oruser_can()if you act on the new site. - Never trust a
blog_idfrom input. Validate it against allowed sites (e.g., sites the current user belongs to, or the network allowlist) before switching. - Keep per-site data isolated. Do not store site A's data in site B's options or tables unless the user explicitly authorized cross-site access.
is_super_admin()is for network-level checks only. Prefercurrent_user_can( 'manage_network' )for capabilities.
Step-by-step implementation
- Check
is_multisite()before running multisite-specific code. - For network admin pages, gate with
current_user_can( 'manage_network_options' ). - For site actions that read a
blog_idfrom input:- Cast to integer.
- Confirm the site exists (
get_site()). - Confirm the current user may act on it (
user_can( $user_id, 'manage_sites' )or membership checks).
- Wrap the operation in
switch_to_blog()/restore_current_blog(). - After switching, re-check capabilities if the action is privileged.
- Return errors rather than silently failing or defaulting to the current site.
Supporting references
| Reference | Load when |
| Multisite security checklist | Before final verification of the multisite security controls. |
| Secure multisite operations | Implementing validated site switching, membership checks, context restoration, and network option updates. |
Common AI mistakes / anti-patterns
Mistake 1 — switch_to_blog() with an unvalidated blog id
// ❌ Insecure: any blog id can be requested, including sites the user does not own.
$blog_id = (int) $_GET['blog_id'];
switch_to_blog( $blog_id );
$posts = get_posts();
restore_current_blog();// ✅ Secure: validate the blog id and the user's relationship to it.
$blog_id = isset( $_GET['blog_id'] ) ? absint( $_GET['blog_id'] ) : 0;
$site = get_site( $blog_id );
if ( ! $site || ! is_user_member_of_blog( get_current_user_id(), $blog_id ) ) {
wp_die( esc_html__( 'Invalid site.', 'my-plugin' ), 403 );
}
switch_to_blog( $blog_id );
$posts = get_posts();
restore_current_blog();Mistake 2 — Forgetting restore_current_blog()
// ❌ Risky: subsequent code runs in the switched context.
foreach ( $blog_ids as $blog_id ) {
switch_to_blog( $blog_id );
do_something();
}// ✅ Secure: restore after each switch.
foreach ( $blog_ids as $blog_id ) {
switch_to_blog( $blog_id );
do_something();
restore_current_blog();
}Mistake 3 — Using manage_options for a network action
// ❌ Insecure: site admins have manage_options but cannot manage the network.
if ( current_user_can( 'manage_options' ) ) {
update_site_option( 'my_plugin_network_key', $value );
}// ✅ Secure: network actions require manage_network / manage_network_options.
if ( ! current_user_can( 'manage_network_options' ) ) {
wp_die( esc_html__( 'You do not have permission to manage network options.', 'my-plugin' ), 403 );
}
update_site_option( 'my_plugin_network_key', $value );Mistake 4 — Leaking data after a switch
// ❌ Insecure: data fetched under site B is echoed on site A without re-checking.
switch_to_blog( $blog_id );
$title = get_bloginfo( 'name' );
restore_current_blog();
echo $title;// ✅ Secure: validate the user may read the target site, then escape output.
if ( ! is_user_member_of_blog( get_current_user_id(), $blog_id ) ) {
return;
}
switch_to_blog( $blog_id );
$title = get_bloginfo( 'name' );
restore_current_blog();
echo esc_html( $title );Mistake 5 — Assuming is_super_admin() covers every case
// ❌ Less precise: is_super_admin checks the super-admin list, not a capability.
if ( is_super_admin() ) {
// do network thing
}// ✅ Secure: prefer the explicit network capability.
if ( current_user_can( 'manage_network' ) ) {
// do network thing
}Mistake 6 — Per-site data in a network-wide cache or option
// ❌ Insecure: site_transient and a global cache group are shared by every
// site, so site A's private member list is served to site B's visitors.
$members = get_site_transient( 'my_plugin_members' );
if ( false === $members ) {
$members = my_plugin_query_members();
set_site_transient( 'my_plugin_members', $members, HOUR_IN_SECONDS );
}// ✅ Secure: per-site storage (or a key that includes the blog ID).
$members = get_transient( 'my_plugin_members' );
if ( false === $members ) {
$members = my_plugin_query_members();
set_transient( 'my_plugin_members', $members, HOUR_IN_SECONDS );
}Also check wp_cache_add_global_groups() calls and custom tables without a
blog_id column: shared storage must never hold per-site private data.
Correct code examples
A complete network-safe routine that validates a blog id, switches context, and restores
it is in references/secure-multisite-operations.php.
Checklist
- Multisite-specific code checks
is_multisite()first. - Network admin actions use
manage_network/manage_network_options. blog_idvalues from input are cast to integers and validated.- The user is confirmed to be a member of / allowed on the target site before switching.
- Every
switch_to_blog()has a matchingrestore_current_blog(). - Capabilities are re-checked after switching sites if the action is privileged.
- Per-site data is not leaked into another site's context.
is_super_admin()is used only where the super-admin list is the correct gate.- Per-site data never goes into
*_site_transient,*_site_option, global cache groups, or shared tables without a blog ID in the key. - Users removed from a site lose access to that site's REST, AJAX, and export paths (
is_user_member_of_blog()rechecked, not cached).
Official references
In this repository26 other files it gives its agents.
- ai-llm-integration-securitySkill
- ajax-securitySkill
- authentication-session-securitySkill
- capability-permission-checksSkill
- cron-background-job-securitySkill
- dependency-supply-chain-securitySkill
- filesystem-securitySkill
- file-upload-securitySkill
- gutenberg-block-editor-securitySkill
- http-api-ssrf-preventionSkill
- input-sanitization-validationSkill
- nonces-csrf-protectionSkill
- object-injection-deserializationSkill
- output-escapingSkill
- rest-api-securitySkill
- secrets-credentials-managementSkill
- secure-plugin-developmentSkill
- security-auditing-code-reviewSkill
- security-headers-cspSkill
- settings-options-securitySkill
- shortcode-block-securitySkill
- sql-injection-preventionSkill
- user-data-protection-privacySkill
- woocommerce-securitySkill
- wp-cli-securitySkill
- wp-hardening-best-practicesSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

