WordPress REST API IDOR: Users, Media, and Plugin Endpoints
WordPress ships a REST API under /wp-json/. Core is imperfect; plugins are where IDOR clusters. If a route returns another user's draft, PII, or private media because the handler only checked is_user_logged_in(), you have a clean object-level bug.
Map routes quickly
Start with GET /wp-json/, then walk /wp/v2/users, /wp/v2/media, posts, comments, and any plugin namespaces listed in the index. Note methods and args. The index often lists routes even when some need auth—treat it as a menu, not as proof of access.
Namespaces to prioritize: job boards, LMS, membership, bookings, wallets, page builders, and "must-use" custom plugins named after the company. WooCommerce and popular form plugins have their own histories; still retest version-specific behavior rather than assuming old CVEs apply.
Create two subscribers (or the lowest roles the site allows). Authenticate with cookies or application passwords if enabled. Compare:
/wp/v2/users/mevs/wp/v2/users/{B}- Media attached to private posts
- Draft posts and revisions
- Plugin objects:
/orders/{id},/students/{id},/messages/{id}
Core locked down user enumeration on many versions, but sites reopen it with plugins or custom rest_authentication_errors filters. Enumerating logins alone may be out of scope or informational—focus on sensitive fields: email, hashed anything, unpaid invoices, course progress.
Plugin handlers skip caps
Read the pattern in vulnerable plugins (when source is public): register_rest_route with 'permission_callback' => '__return_true' or a callback that checks login but not ownership. For closed plugins, behavior is enough—swap the id, keep the cookie.
Batch routes and ?include= / ?slug= filters sometimes bypass singular checks. Try context=edit on objects you should only see in view. Multisite adds blog-switching twists—confirm you are not accidentally reading another site's content under a shared user table when that is in scope.
Application passwords and cookie auth differ in headers. Some plugins authorize one and forget the other. JWT plugins introduce Authorization: Bearer paths with their own IDOR flavors; test both if both are enabled.
Unauthenticated POST routes that create objects with a client-supplied author field are create-IDOR cousins—worth a short paragraph if you can assign content to another user id without being an editor.
Search and autocomplete endpoints deserve a pass too. A typeahead that returns emails or order numbers for partial queries can leak across customers while the singular resource route looks locked down. Hit them authenticated as A with fragments unique to B's canary data.
Custom post types registered as show_in_rest => true without capping view versus edit fields often expose ACF/meta keys the theme never intended to ship publicly. Spot-check one CPT you created yourself before claiming sitewide impact.
Report without dumping the site
File a report with role used, route, A/B ids, and the private field observed. Avoid dumping the full user table. Note WordPress and plugin versions from readme or generators when available. If caching plugins serve context=edit responses to logged-out users, capture cache headers—that is a separate but related finding.
Fix guidance: permission_callback tied to current_user_can and per-object authorship checks, disable unused namespaces, and keep WordPress plus plugins patched. The REST handbook explains intended authz—your finding is where a plugin ignored it.