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
}
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.