Come usare
- Copia il comando e controlla il pacchetto prima di installarlo nel tuo ambiente agente.
- Controlla strumenti, credenziali e ambienti richiesti nel documento originale.
- Descrivi input e risultato desiderato; confronta l’output con le fonti.
npx skills add https://github.com/wpultimatesecurity/wordpress-security-skills --skill multisite-securityPrima di iniziare
- L’installazione non include abbonamenti, quote API o accesso ai file.
- Stelle e installazioni indicano attività, non un test indipendente di qualità o sicurezza.
Contenuto dal repository con titoli, esempi, codice, tabelle, link e immagini preservati.
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()` with `restore_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-verify current_user_can() or user_can() if you act on the new site.
- Never trust a `blog_id` from 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. Prefer
current_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).



