WP Manifestindependent plugin directory
manifest / multisite / network-wp-query

Network WP Query WP Registry grade D+. High-severity findings · 2 findingsWP RegistryHigh-severity findings · 2 findingsOpen the reportD+

:electric_plug: Network-wide WP Query for Multisite environment, which supports POSTS-PER-SITE query etc. [STAND-ALONE][Plugin]

★ 16stars
1.4kcomposer installs
v0.2.0latest · 9 years ago

Install

Download source zip No release zip yet

This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/devaloka/network-wp-query/archive/refs/heads/master.zip

On Packagist as devaloka/network-wp-query:

composer require devaloka/network-wp-query
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

Latest Stable Version Latest Unstable Version License Build Status

A WordPress plugin that provides Network-wide WP_Query for Multisite environment.

This plugin is based on / a improved version of WP_Query_Multisite (a custom version of WP_Query_Multisite).

Installation

Manual Installation

  1. Just copy all files into <ABSPATH>wp-content/plugins/network-wp-query/.

Manual Installation (as a Must-Use plugin)

  1. Just copy all files into <ABSPATH>wp-content/mu-plugins/network-wp-query/.

  2. Move network-wp-query/loader/50-network-wp-query-loader.php into <ABSPATH>wp-content/mu-plugins/.

Installation via Composer

  1. Install via Composer.

    composer require devaloka/network-wp-query

Installation via Composer (as a Must-Use plugin)

  1. Install via Composer.

    composer require devaloka/network-wp-query
  2. Move network-wp-query directory into <ABSPATH>wp-content/mu-plugins/.

  3. Move network-wp-query/loader/50-network-wp-query-loader.php into <ABSPATH>wp-content/mu-plugins/.

Example Usage

Standard Loop

<?php $query = new WP_Query(['network' => true]); ?>

<?php if ($query->have_posts()): ?>
    <?php while ($query->have_posts()): $query->the_post(); ?>
        <?php the_content(); ?>
    <?php endwhile; ?>

    <?php wp_reset_postdata(); ?>
<?php endif; ?>

Query to several specific Sites

<?php $query = new WP_Query(['network' => true, 'sites__in' => [1, 2, 3]]); ?>

<?php if ($query->have_posts()): ?>
    <?php while ($query->have_posts()): $query->the_post(); ?>
        <?php the_content(); ?>
    <?php endwhile; ?>

    <?php wp_reset_postdata(); ?>
<?php endif; ?>

Query excluding several specific Sites

<?php $query = new WP_Query(['network' => true, 'sites__not_in' => [1, 2, 3]]); ?>

<?php if ($query->have_posts()): ?>
    <?php while ($query->have_posts()): $query->the_post(); ?>
        <?php the_content(); ?>
    <?php endwhile; ?>

    <?php wp_reset_postdata(); ?>
<?php endif; ?>

Limit the number of posts per Site

<?php $query = new WP_Query(['network' => true, 'posts_per_site' => 1]); ?>

<?php if ($query->have_posts()): ?>
    <?php while ($query->have_posts()): $query->the_post(); ?>
        <?php the_content(); ?>
    <?php endwhile; ?>

    <?php wp_reset_postdata(); ?>
<?php endif; ?>

Parameters

Name Type Description
network boolint Whether perform network-wide query.
sites__in int[] Blog IDs to include in the query.
sites__not_in int[] Blog IDs to excluded from the query.
posts_per_site int The number of posts per Site to retrieve.

References

Releases

5 releases.

Tag
Published
v0.2.0 latest
Apr 9, 2017 9 years ago
Apr 2, 2016 10 years ago
Jan 8, 2016 10 years ago
Dec 31, 2015 10 years ago
Dec 31, 2015 10 years ago

These releases are tags only. The author does not attach a packaged zip, so there are no download counts to report.

D+ grade

Security

Network WP Query 0.2.0 · audited by WP Registry

High-severity findings.

1 high 1 medium
Audited release
0.2.0
Findings
2
Worst severity
high
Content hash
90e4d6c10353ace79063eeb9…

Findings

  • high Public `network` query var UNIONs every blog's posts, including private and deleted-site content

    information_disclosure

    Subscriber::subscribe() adds network, sites__in, sites__not_in, and posts_per_site to WordPress public query vars. Those keys are therefore populated from $_GET into the main WP_Query (WP::parse_request). They are not needed for the documented `new WP_Query(['network' => true])` usage — programmatic query args already survive parse_query — so this is an unintended unauthenticated frontend API: /?network=1, /?s=term&network=1, /?feed=rss2&network=1, or /?network=1&sites__in[]=N. setUpQuery() then loads every blog_id with SELECT blog_id FROM {$wpdb->blogs} and never filters archived, spam, deleted, or public=0, unlike get_sites()/ms_site_check(). modifyClauses() clones the current query's JOIN/WHERE (already decided from current-blog current_user_can('read_private_posts')) onto each site via table-prefix str_replace and UNION ALL. Consequences: (1) unauthenticated visitors receive publish-status posts from sites WordPress core would refuse to bootstrap (deleted/archived/spam) and from every other tenant; (2) an Editor or per-site administrator who is not a member of site B still gets site B's private posts, because the private-status OR clause is not recomputed under switch_to_blog(); (3) the loop calls switch_to_blog($post->site_ID) so the_content()/the_title() render that foreign row. This breaks Multisite's per-site capability and deleted-site isolation without any nonce or capability gate.

    src/Subscriber.php:42-67

    Recommendation

    Stop registering these keys on the query_vars filter so they cannot be set from the URL; honor them only when passed in a WP_Query argument array from trusted PHP. Load site IDs with get_sites(['archived'=>0,'deleted'=>0,'spam'=>0]) (and skip public=0 unless the caller opted in). Inside the per-site UNION, rebuild or strip private/protected statuses unless the user is a member of that blog and current_user_can the matching cap after switch_to_blog(). Document that network-wide queries are an explicit theme/plugin opt-in, not a GET flag.

  • medium Unauthenticated unbounded per-site LIMIT via public `posts_per_site` query var

    dos

    posts_per_site is on the same public query_vars list as network. modifyClauses() does (int) $query->get('posts_per_site', $postsPerPage) and interpolates the value into each per-site SELECT as LIMIT 0, {$postsPerSiteForTheSite} with no absint() floor, no hard cap, and no refusal of values that exceed the table. WordPress core keeps posts_per_page in private_query_vars specifically so visitors cannot do this from the URL; this plugin reintroduces that class of control as a public arg and then multiplies it across every blog in the UNION (plus SQL_CALC_FOUND_ROWS on the outer query). A request such as /?network=1&posts_per_site=1000000 forces MySQL to materialize up to N rows from each site's posts table before the outer LIMIT trims the PHP result. On a large network this is request-level resource exhaustion of the database worker. Negative values are truthy in PHP so LIMIT 0, -1 is also reachable and errors the main query (500).

    src/NetworkWpQuery.php:225-247

    Recommendation

    Do not expose posts_per_site as a public query var. Coerce with max(1, absint(...)), cap it (e.g. min($n, 100) or the current posts_per_page), and skip the site if the value is not a positive integer after the posts_per_site filter. Keep the inner LIMIT aligned with the outer WP_Query limits.

WP Registry hashes the installable build and reports on that exact bytes-for-bytes copy. Embargoed findings are withheld until they are disclosed, so a clean verdict means nothing public is outstanding. WP Manifest does not audit code itself.