wpultimatesecurity/wordpress-security-skills

multisite-security

Use when writing code that runs on a WordPress multisite network - switchtoblog, network admin pages, getsites, or capabilities that differ between site and network scope.

Zobacz źródło

Jak używać

  1. Skopiuj polecenie instalacji i sprawdź źródła przed instalacją we własnym środowisku agenta.
  2. W oryginale sprawdź wymagane narzędzia, dane dostępowe i obsługiwane środowiska.
  3. Opisz dane wejściowe i oczekiwany rezultat; porównaj wynik ze źródłami.
npx skills add https://github.com/wpultimatesecurity/wordpress-security-skills --skill multisite-security

Przed rozpoczęciem

  • Instalacja nie obejmuje subskrypcji modelu, limitu API ani uprawnień do plików.
  • Gwiazdki i instalacje pokazują aktywność, a nie niezależny test jakości lub bezpieczeństwa.
Oryginalny dokument Skill

Treść z repozytorium z zachowaniem nagłówków, przykładów, kodu, tabel, linków i obrazów.

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_id from 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)

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

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

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

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

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

  1. `is_super_admin()` is for network-level checks only. Prefer current_user_can( 'manage_network' )

for capabilities.

Step-by-step implementation

  1. Check is_multisite() before running multisite-specific code.
  2. For network admin pages, gate with current_user_can( 'manage_network_options' ).
  3. For site actions that read a blog_id from 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).

  1. Wrap the operation in switch_to_blog() / restore_current_blog().
  2. After switching, re-check capabilities if the action is privileged.
  3. Return errors rather than silently failing or defaulting to the current site.

Supporting references

ReferenceLoad when
Multisite security checklistBefore final verification of the multisite security controls.
Secure multisite operationsImplementing 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

php
// ❌ 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();
php
// ✅ 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()

php
// ❌ Risky: subsequent code runs in the switched context.
foreach ( $blog_ids as $blog_id ) {
    switch_to_blog( $blog_id );
    do_something();
}
php
// ✅ 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

php
// ❌ 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 );
}
php
// ✅ 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

php
// ❌ 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;
php
// ✅ 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

php
// ❌ Less precise: is_super_admin checks the super-admin list, not a capability.
if ( is_super_admin() ) {
    // do network thing
}
php
// ✅ 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

php
// ❌ 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 );
}
php
// ✅ 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_id values 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 matching restore_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

Powiązane zadania

z tego samego repozytorium

Więcej Skills

Wszystkie Skills
wpultimatesecurity
Społeczność

capability-permission-checks

Use when adding admin pages, menu items, AJAX/REST handlers, action links, or any code that performs a privileged operation in WordPress. Gates actions with currentusercan() using the correct capability (not roles), including per-object checks like editpost, and pairs the check with a nonce. Prevents privilege escalation and broken access control. Apply proactively to every privileged code path.

instalacje
1
GitHub Stars
50
Aktualizacja
29 wrz
wpultimatesecurity
Społeczność

filesystem-security

Use when reading, writing, including, or deleting files from paths that include user input - include / require, readfile, unlink, filegetcontents, or the WPFilesystem API. Validates paths with validatefile, normalizes with wpnormalizepath, confines operations to an allowed base directory, and uses wpdeletefile / WPFilesystem. Prevents path traversal, local file inclusion, and arbitrary file deletion.

instalacje
1
GitHub Stars
50
Aktualizacja
29 wrz
wpultimatesecurity
Społeczność

nonces-csrf-protection

Use when handling any form submission, AJAX request, admin-post action, settings page, link that triggers an action, or any other user-initiated request in a WordPress plugin or theme. Generates nonces with wpnoncefield / wpcreatenonce and verifies them with checkadminreferer, checkajaxreferer, or wpverifynonce, always paired with a capability check, to prevent CSRF. Apply proactively whenever code accepts or acts on a request.

instalacje
1
GitHub Stars
50
Aktualizacja
29 wrz
wpultimatesecurity
Społeczność

secrets-credentials-management

Use when handling passwords, API keys, tokens, or third-party credentials in a WordPress plugin or theme. Hashes passwords with wphashpassword / wpcheckpassword, generates tokens with wpgeneratepassword, keeps secrets out of code and the database in plaintext, and uses Application Passwords for API auth. Prevents credential leakage and insecure storage.

instalacje
1
GitHub Stars
50
Aktualizacja
29 wrz