Skip to content

Security: require capability checks on privileged network-admin AJAX endpoints#1371

Merged
superdav42 merged 5 commits into
Ultimate-Multisite:mainfrom
vuckro:security/ajax-capability-checks
Jun 18, 2026
Merged

Security: require capability checks on privileged network-admin AJAX endpoints#1371
superdav42 merged 5 commits into
Ultimate-Multisite:mainfrom
vuckro:security/ajax-capability-checks

Conversation

@vuckro

@vuckro vuckro commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Several wp_ajax_* endpoints that back network-admin tools were registered
with no capability check, so any authenticated user (including a subscriber on a
sub-site) could reach them. None of these are wired to customer-facing UI.

This enforces manage_network on each, and adds two endpoint-specific hardenings.

Changes

  • Ajax::search_models / search_all_models — returned network-wide objects
    and, for the user model, WordPress logins and email addresses
    (user/email enumeration). Restricted to network admins.
  • View_Logs_Admin_Page::handle_view_logs — capability check and the
    "is this under the logs folder?" substring test replaced with realpath()
    containment so a crafted path can no longer traverse out of the logs
    directory (arbitrary file read).
  • Dashboard_Widgets::process_ajax_fetch_rss — capability check and the
    outbound feed URL is now pinned to the plugin's own community feed
    (filterable), removing an SSRF vector; plus handle_table_csv.
  • System_Info_Admin_Page::generate_text_file_system_info — system report.
  • Domain_Manager::get_dns_records / test_integration — DNS lookups and
    hosting-provider connection tests.
  • Site_Manager::get_site_screenshot — screenshot scraper.
  • Template_Placeholders::save_placeholders / serve_placeholders_via_ajax.
  • Base_Customer_Facing_Admin_Page customize form: capability exist
    raised to manage_network.

Compatibility

These endpoints are only ever invoked from network-admin screens, so legitimate
use is unaffected. The customer-facing DNS flow uses a different action
(wu_get_dns_records_for_domain) and is not touched.


Part of a small series of focused security hardening PRs. Full technical detail
is available privately to the maintainers on request (coordinated disclosure).

Summary by CodeRabbit

  • Security
    • Restricted multiple network-admin tools and AJAX endpoints to manage_network, returning HTTP 403 when unauthorized.
    • Strengthened log file access using resolved-path containment checks.
    • Added/required AJAX nonces for DNS lookups, integration tests, placeholder editing, CSV export, and screenshot requests.
    • Hardened RSS feed handling to prevent request-controlled outbound URLs (SSRF mitigation).
  • Frontend
    • Improved DNS table script scoping by attaching the Vue instance/state to window.
    • Updated several frontend requests to send the appropriate nonce values in payloads.

… endpoints

Several network-admin AJAX endpoints were registered on wp_ajax_* with no
capability check, so any authenticated user (including a subscriber on a
sub-site) could reach them. None of these are wired to customer-facing UI;
they all back network-admin tools. This enforces manage_network on:

- Ajax::search_models / search_all_models — returned network-wide objects
  and, for the 'user' model, WordPress logins and email addresses
  (user/email enumeration).
- View_Logs_Admin_Page::handle_view_logs — also replaces the substring
  "is it under the logs folder?" check with realpath() containment so a
  crafted path can no longer traverse out of the logs directory and read
  arbitrary files (e.g. wp-config.php).
- System_Info_Admin_Page::generate_text_file_system_info — system report.
- Dashboard_Widgets::process_ajax_fetch_rss — also pins the outbound feed
  URL to the plugin's own community feed (filterable) so the endpoint can
  no longer be used as an SSRF probe; and handle_table_csv.
- Domain_Manager::get_dns_records and ::test_integration — DNS lookups and
  hosting-provider connection tests.
- Site_Manager::get_site_screenshot — screenshot scraper.
- Template_Placeholders::save_placeholders / serve_placeholders_via_ajax.
- Base_Customer_Facing_Admin_Page customize form: capability 'exist'
  (any logged-in user) raised to 'manage_network'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 78aa01ed-6686-435d-8211-2ea80f93ef51

📥 Commits

Reviewing files that changed from the base of the PR and between 7b634b4 and 02a06ed.

📒 Files selected for processing (8)
  • assets/js/dns-table.js
  • assets/js/integration-test.js
  • assets/js/screenshot-scraper.js
  • inc/admin-pages/class-domain-edit-admin-page.php
  • inc/admin-pages/class-hosting-integration-wizard-admin-page.php
  • inc/admin-pages/class-site-edit-admin-page.php
  • inc/managers/class-domain-manager.php
  • inc/managers/class-site-manager.php

📝 Walkthrough

Walkthrough

Adds manage_network capability checks to multiple admin page handlers, AJAX endpoints, and manager methods. Replaces a case-insensitive substring path check in the view-logs handler with a realpath()-based traversal guard. Mitigates an SSRF vector in the RSS dashboard widget by forcing the feed URL through a filter instead of accepting it from request parameters. Implements AJAX nonce verification across AJAX handlers and scripts.

Changes

manage_network Capability Hardening and Path/SSRF/Nonce Fixes

Layer / File(s) Summary
Admin page capability guards and log-listing refactor
inc/admin-pages/class-base-customer-facing-admin-page.php, inc/admin-pages/class-system-info-admin-page.php, inc/admin-pages/class-view-logs-admin-page.php
Edit-admin-page form registration uses manage_network instead of exist; generate_text_file_system_info() and handle_view_logs() both add early manage_network checks (HTTP 403 on failure); handle_view_logs() caches the logs folder path in $logs_folder replacing repeated Logger::get_logs_folder() calls.
realpath-based log-file path traversal fix
inc/admin-pages/class-view-logs-admin-page.php
Replaces the case-insensitive stristr check on the requested log path with a realpath()-based containment check, denying access with HTTP 403 if the resolved path escapes the logs directory and rewriting $file to the resolved real path on success.
AJAX and manager endpoint capability and nonce guards
inc/class-ajax.php, inc/class-dashboard-widgets.php, inc/managers/class-domain-manager.php, inc/managers/class-site-manager.php
Ajax::search_models() and handle_table_csv() add manage_network checks; process_ajax_fetch_rss() adds manage_network check and forces $atts['url'] through apply_filters('wu_dashboard_rss_feed_url', $default_url) to prevent SSRF; Domain_Manager::get_dns_records() and test_integration() add manage_network checks and AJAX nonce verification (wu_get_dns_records, wu_test_hosting_integration); Site_Manager::get_site_screenshot() adds manage_network check and AJAX nonce verification (wu_get_screenshot), all returning HTTP 403 JSON WP_Error responses on failure.
Template placeholder AJAX capability and nonce guards
inc/site-templates/class-template-placeholders.php
serve_placeholders_via_ajax() and save_placeholders() both add early manage_network checks, returning HTTP 403 JSON errors on failure; save_placeholders() also updates check_ajax_referer to pass false as the die parameter.
Nonce generation and frontend script integration
inc/admin-pages/class-domain-edit-admin-page.php, inc/admin-pages/class-hosting-integration-wizard-admin-page.php, inc/admin-pages/class-site-edit-admin-page.php, assets/js/dns-table.js, assets/js/integration-test.js, assets/js/screenshot-scraper.js
Admin pages localize nonce values (wu_get_dns_records, wu_test_hosting_integration, wu_get_screenshot) for their front-end scripts; dns-table.js, integration-test.js, and screenshot-scraper.js add global variable annotations and include _ajax_nonce in AJAX POST payloads sourced from localized nonce data.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐇 Hopping through the gates at night,
A bunny checked each lock was tight.
manage_network? You may pass!
Without it? 403, alas.
No path tricks, no SSRF sneak —
The warren's safe from hare to peak! 🔒

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately reflects the main objective of the PR: requiring capability checks on privileged network-admin AJAX endpoints for security hardening.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
inc/site-templates/class-template-placeholders.php (1)

164-179: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Return WP_Error JSON failures for authorization and nonce errors.

The new authorization branch returns a custom array via wp_send_json(), which produces a different failure shape from the other hardened endpoints. Use wp_send_json_error(new \WP_Error(...), 403) here; the nonce failure can be aligned at the same time.

♻️ Proposed response-shape alignment
 if ( ! current_user_can('manage_network')) {
-	wp_send_json(
-		[
-			'code'    => 'not-enough-permissions',
-			'message' => __('You don\'t have permission to alter placeholders.', 'ultimate-multisite'),
-		]
-	);
+	wp_send_json_error(
+		new \WP_Error('not-enough-permissions', __('You don\'t have permission to alter placeholders.', 'ultimate-multisite')),
+		403
+	);
 }

 if ( ! check_ajax_referer('wu_edit_placeholders_editing', false, false)) {
-	wp_send_json(
-		[
-			'code'    => 'not-enough-permissions',
-			'message' => __('You don\'t have permission to alter placeholders.', 'ultimate-multisite'),
-		]
-	);
+	wp_send_json_error(
+		new \WP_Error('not-enough-permissions', __('You don\'t have permission to alter placeholders.', 'ultimate-multisite')),
+		403
+	);
 }

As per coding guidelines, “Use WP_Error for validation/operation failures — not exceptions. Check is_wp_error() on return values.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@inc/site-templates/class-template-placeholders.php` around lines 164 - 179,
The two permission checks in this block (the current_user_can for
'manage_network' permission and the check_ajax_referer for
'wu_edit_placeholders_editing' nonce) are using inconsistent custom array
responses via wp_send_json(). Replace both instances with wp_send_json_error(new
\WP_Error(...), 403) to maintain consistent error response shapes across all
authorization endpoints. For the WP_Error constructor, use an appropriate error
code (such as 'not-enough-permissions') as the first parameter and the
translated message as the second parameter, ensuring both authorization and
nonce failures return the same structured WP_Error format with a 403 status
code.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@inc/managers/class-domain-manager.php`:
- Around line 1229-1231: The get_dns_records() and test_integration() methods in
the Domain_Manager class lack CSRF protection despite having capability checks.
After the existing current_user_can('manage_network') capability check in both
methods, add a check_ajax_referer() call to verify the nonce. Use
wp_send_json_error() with a 403 HTTP status code if the nonce verification
fails. Then update the corresponding JavaScript callers in dns-table.js and
integration-test.js to include the nonce in their AJAX requests by adding the
nonce to the data parameter being sent with each AJAX call.

In `@inc/managers/class-site-manager.php`:
- Around line 542-544: The get_site_screenshot() AJAX handler lacks nonce
verification, making it vulnerable to CSRF attacks. Add a check_ajax_referer()
call after the current_user_can() capability check in the handler to verify the
nonce with the action 'wu_get_screenshot'. On the client-side in
assets/js/screenshot-scraper.js, update the AJAX request data object to include
the nonce by adding a field with the value generated from
wp_create_nonce('wu_get_screenshot') on the server. This ensures that only
legitimate requests from your application can trigger the screenshot
functionality.

---

Outside diff comments:
In `@inc/site-templates/class-template-placeholders.php`:
- Around line 164-179: The two permission checks in this block (the
current_user_can for 'manage_network' permission and the check_ajax_referer for
'wu_edit_placeholders_editing' nonce) are using inconsistent custom array
responses via wp_send_json(). Replace both instances with wp_send_json_error(new
\WP_Error(...), 403) to maintain consistent error response shapes across all
authorization endpoints. For the WP_Error constructor, use an appropriate error
code (such as 'not-enough-permissions') as the first parameter and the
translated message as the second parameter, ensuring both authorization and
nonce failures return the same structured WP_Error format with a 403 status
code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 75c463b9-2c6c-41bb-af49-cfef2ff88a07

📥 Commits

Reviewing files that changed from the base of the PR and between 4880d1f and 7b634b4.

📒 Files selected for processing (8)
  • inc/admin-pages/class-base-customer-facing-admin-page.php
  • inc/admin-pages/class-system-info-admin-page.php
  • inc/admin-pages/class-view-logs-admin-page.php
  • inc/class-ajax.php
  • inc/class-dashboard-widgets.php
  • inc/managers/class-domain-manager.php
  • inc/managers/class-site-manager.php
  • inc/site-templates/class-template-placeholders.php

Comment thread inc/managers/class-domain-manager.php
Comment thread inc/managers/class-site-manager.php
@superdav42

Copy link
Copy Markdown
Collaborator

CLAIM_RELEASED reason=worker_complete runner=superdav42 ts=2026-06-17T02:18:36Z aidevops_version=3.20.86 opencode_version=1.17.7

@superdav42 superdav42 added the status:available Task is available for claiming label Jun 17, 2026
@superdav42

Copy link
Copy Markdown
Collaborator

Stuck-merge detector: PR has been merge-eligible but unmerged past the threshold

The pulse merge pass has classified PR #1371 as STUCK_OTHER and it has been sitting unmerged longer than AIDEVOPS_MERGE_STUCK_AGE_MINUTES (currently 240m). The deterministic merge gates are evaluated every cycle (~120s) and this PR has consistently failed them.

Failing checks on PR #1371

  • (no FAILURE entries in rollup; check rollup manually)

Worker guidance for the next attempt

  1. Read PR Security: require capability checks on privileged network-admin AJAX endpoints #1371 body + the latest check run logs:
    gh pr checks 1371 --repo Ultimate-Multisite/ultimate-multisite
  2. If the failing checks are environment/Setup-step (Format, Lint, Typecheck all FAIL at the same step), the canonical default branch likely has a broken lockfile or a CI infra change — fix at the base, not on this PR. Look for a sibling outage meta-issue in this repo (filed by the same detector) before forking off here.
  3. If the failures are PR-specific (e.g. a Typecheck error introduced by this PR's code), rebase onto the latest default branch and address the diagnosed errors. Use full-loop-helper.sh start from the linked PR's worktree.
  4. If the linked issue body lacks the worker-ready file paths and verification commands required by t1900, post a comment naming the missing context before dispatching another worker — the next attempt will burn tokens on exploration otherwise.

Why you're seeing this

Every pulse cycle (~120s) the deterministic merge pass re-evaluates open PRs. PRs that pass APPROVED + MERGEABLE but fail required checks have historically been re-evaluated silently every cycle until a human noticed. The stuck-merge detector (t3193) surfaces them after AIDEVOPS_MERGE_STUCK_AGE_MINUTES minutes idle. This comment is posted exactly once per linked issue — repeated stuck cycles will NOT spam the thread. If the PR merges and the issue is reopened later with a fresh stuck PR, the marker will allow a second comment.

Posted automatically by pulse-merge-stuck.sh (t3193 / GH#21895). Threshold env: AIDEVOPS_MERGE_STUCK_AGE_MINUTES=240.


aidevops.sh v3.20.86 automated scan.

@superdav42 superdav42 merged commit 9781258 into Ultimate-Multisite:main Jun 18, 2026
10 checks passed
@superdav42 superdav42 added the review-feedback-scanned Merged PR already scanned for quality feedback label Jun 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-feedback-scanned Merged PR already scanned for quality feedback status:available Task is available for claiming

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants