Skip to content
Noroxi
Research10 min read

Anywhere Elementor 1.3.0: broken access control in AJAX

A front-end AJAX nonce and missing capability checks let unauthenticated visitors read non-public content and low-privilege users write a site-wide option.

Noroxi TeamPenetration testing and security consulting

Abstract. Anywhere Elementor 1.3.0, a WordPress page-building helper with a reported 70,000+ installs, ships a cluster of broken access-control defects in its AJAX layer. The shared root cause is an AJAX nonce emitted into the HTML of every front-end page for every visitor: once a nonce guards a door everyone can see, it guards nothing. Pair that with handlers that run no object-level or capability check and the result is unauthenticated content disclosure plus a low-privilege option write. This write-up covers the bug class, the root-cause pattern, how the vulnerable sink is reached at a conceptual level, the impact, and concrete remediation and detection guidance. Following coordinated disclosure, the full operational proof of concept stays withheld until a fix ships; everything here is defensive and educational.

Keywords: WordPress, broken access control, IDOR, nonce misuse, capability checks, authorization, secure coding.

1. Introduction

Anywhere Elementor extends the Elementor page builder so that builder-authored content can be rendered through shortcodes and AJAX endpoints anywhere on a site. With a reported 70,000+ active installations, it runs on a large number of production sites, and many of those sites keep non-public content — drafts, private posts, templates, per-author documents — right alongside their public pages.

None of the defects described here is a headline remote-code-execution bug. Each one is an authorization gap, and authorization gaps are the quiet kind of failure. They leak non-public content, expose internal field schemas, and let low-privilege users flip site-wide settings, all while the plugin keeps behaving normally for legitimate users. That is exactly why they survive code review. The pattern below is one every WordPress site owner and plugin developer should be able to recognise on sight.

2. Affected component

PluginAnywhere Elementor — slug anywhere-elementor
Tested version1.3.0 (current at time of testing)
Active installations70,000+
Vulnerability classBroken access control — unauthenticated/low-privilege IDOR content disclosure and a low-privilege option write (CWE-862 Missing Authorization, CWE-639 Authorization Bypass Through User-Controlled Key)
Honest severityMedium (per the most impactful issues); several contributing issues are Low
CVSSNot assigned at time of writing — no public score invented
StatusFresh; confirmed in source on the current version (front-end nonce reachability and missing-capability gates verified on an isolated localhost lab); unpatched

Several endpoints share the same weakness:

  • A. ae_post_data — renders arbitrary non-public Elementor content by id (unauthenticated).
  • B. ae_repeater_data — reads ACF repeater field values by id (unauthenticated).
  • C. [INSERT_ELEMENTOR id=..] shortcode — a Contributor/Author can render any Elementor document by id.
  • D. save_modules (aep_module) — a Subscriber (or CSRF against an admin) writes the plugin's module-activation option; no nonce and no capability check.
  • E. ae_prev_post / ae_prev_term / ae_acf_repeater_fields / ae_acf_flexible_fields — a Subscriber enumerates ACF field schema / previews content by id (missing capability).
  • F. ae_term_data — enumerates terms of any taxonomy, including non-public ones (unauthenticated).

3. Technical analysis

Two distinct root causes run through the cluster.

3.1 A front-end nonce mistaken for an authentication barrier

The plugin's AJAX nonce (aep_ajax_nonce) is localized into a JavaScript object (aepro.aep_nonce) on every front-end page, for every visitor; the enqueue fires on wp_enqueue_scripts with no authentication gate at all. The nopriv AJAX handlers then validate incoming requests with wp_verify_nonce('aep_ajax_nonce') and nothing more.

A WordPress nonce is a CSRF token. It is not an authentication or authorization mechanism. Print the same nonce into public HTML that anyone can read, and verifying it proves one thing only: the request carried a value that was already published to the whole world. The defect shows up cleanly in this shape:

php
// Illustrative of the defective pattern (ae_post_data / ae_repeater_data / ae_term_data).// Registered for logged-out visitors:add_action( 'wp_ajax_nopriv_ae_post_data', 'ajax_post_data' );
function ajax_post_data() {    // The ONLY gate: a nonce that was localized into every public page.    if ( ! wp_verify_nonce( $_POST['nonce'], 'aep_ajax_nonce' ) ) {        wp_die();    }    // No post-status / post-type / ownership / capability check on $_POST['post_id'].    $id = (int) $_POST['post_id'];    echo get_builder_content( $id );   // renders drafts/private/other-author content    wp_die();}

The mistake is treating a public CSRF token as proof of identity or permission. Because the token is public, the nopriv handler is effectively unauthenticated, and the attacker-controlled post_id (CWE-639) is rendered with no object-level authorization behind it (CWE-862). The ACF-value reader (B) and the taxonomy enumerator (F) follow the same shape. The shortcode renderer (C) is a variant: it accepts an id behind a bare existence check (get_post() !== null), with no verification of status, type, password, ownership, or capability, so a Contributor who can preview their own draft can render any document by id.

3.2 A state-changing handler with neither a nonce nor a capability check

The save_modules handler, registered as wp_ajax_aep_module, writes $_POST['moduleData'] into the aep_modules option via update_option. It performs no check_ajax_referer and no current_user_can:

php
// Illustrative of the defective pattern (save_modules).add_action( 'wp_ajax_aep_module', 'save_modules' );
function save_modules() {    // No check_ajax_referer(...) and no current_user_can(...) here.    // (The plugin's own phpcs:ignore for NonceVerification.Missing confirms the omission.)    update_option( 'aep_modules', $_POST['moduleData'] ); // site-wide feature flags    wp_die();}

A sibling handler, save_config, is correctly nonce-gated, which makes the omission conspicuous: the authorization was understood and applied elsewhere in the same codebase, just not here. The written values are coerced to boolean, so the practical impact is settings and feature-flag tampering reachable via CSRF rather than stored XSS or code execution. It remains a site-wide state change driven by an unprivileged actor.

4. How the flaw is reached (conceptual)

The mechanism is simple at a teaching level, and it needs no special position on the network.

Unauthenticated IDOR (A, B, F). The handlers lean on a nonce, and that nonce is published in the HTML the plugin sends to every visitor on ordinary front-end pages. "Holding a valid nonce" is therefore not a privilege; it is something any visitor already has the moment a page loads. Once a request clears the nonce check, the handler renders or returns content addressed by a numeric id alone, and it never asks whether the caller is allowed to see that object. An id pointing at a draft, a private post, or another author's document is handled exactly like a public one, and content that was never meant to be public comes back.

Shortcode (C). A user who is only a Contributor can legitimately create and preview their own draft. The renderer, invoked from that draft, resolves whatever id it is handed and confirms only that some post exists for it. It never confirms the id belongs to a document the Contributor is allowed to view, so previewing the draft renders the referenced document in full.

Option write (D). The handler that stores the plugin's module flags runs with no nonce and no capability check. The lowest authenticated role — a Subscriber — can reach it directly, and with no nonce in place it is also reachable through cross-site request forgery aimed at a logged-in administrator.

One thread ties all of this together: the code confirms that a request is well-formed, yet never confirms that the caller is authorized to perform this action on this object. We deliberately omit the literal request sequence, the parameter values, and any runnable commands. A defender needs none of that to understand the risk or shut it down.

5. Impact assessment

Taken together, the cluster is systemic broken access control in the plugin's AJAX layer.

  • Confidentiality. Unauthenticated or low-privilege disclosure of non-public Elementor content (A, C), ACF repeater values (B), ACF field schema and arbitrary post/term previews (E), and taxonomy terms including non-public taxonomies (F). In practice this can leak unpublished marketing pages, internal templates, customer data stored in ACF fields, and the shape of a site's content.
  • Integrity. A Subscriber, or an administrator hit by CSRF, can toggle the plugin's module-activation flags site-wide (D). The values are boolean, so this is feature-flag and settings tampering rather than code execution, but an unprivileged actor still gets to change how the plugin behaves for every visitor.
  • Availability. Not directly affected.

No single issue is critical on its own, which is why the honest per-issue severity lands at Medium for the most impactful endpoints (A, C, D) and Low for the rest (B, E, F). We assign no CVSS score; none has been published, and inventing one would be dishonest. The combination is the real story — several independent authorization gaps in one component, each reachable with little or no privilege.

6. Remediation

The fix is to stop treating a public nonce as authorization and to add real, object-level checks everywhere a user-controlled id selects the object:

php
// Suggested remediation — render / IDOR handlers (A, B, C, E, F):function ajax_post_data() {    // 1) A real capability gate, not just a nonce.    if ( ! current_user_can( 'edit_posts' ) ) {        wp_send_json_error( 'forbidden', 403 );    }    $id = (int) $_POST['post_id'];    // 2) Object-level authorization for THIS object.    if ( ! current_user_can( 'edit_post', $id ) ) {        wp_send_json_error( 'forbidden', 403 );    }    // 3) Apply Elementor's own view policy to non-public / library documents.    echo get_builder_content( $id );    wp_die();}
// Suggested remediation — state-changing handler (D):function save_modules() {    if ( ! current_user_can( 'manage_options' ) ) {        wp_send_json_error( 'forbidden', 403 );    }    check_ajax_referer( 'aep_ajax_nonce' ); // CSRF token IN ADDITION to the cap check    // ... validate/whitelist keys, then update_option( 'aep_modules', ... );    wp_die();}

Concretely, the vendor should:

  1. Drop the public-page nonce as the sole gate on the render/IDOR handlers and add per-capability checks (current_user_can('edit_posts') plus object-level edit_post/status checks).
  2. Enforce current_user_can('manage_options') and check_ajax_referer on save_modules.
  3. Apply Elementor's own view policy — restricting elementor_library and non-public documents — in both the shortcode and ae_post_data.

Site owners should move to a fixed release as soon as the vendor publishes one. No fixed version exists at the time of writing.

7. Detection and hardening for site owners

Until a fixed release lands, the measures below follow directly from the mechanics above, and none of them assumes a product feature that does not exist:

  • Least privilege. Review who holds Subscriber, Contributor, and Author roles. With open registration enabled, assume untrusted parties can obtain a low-privilege account; disable self-registration wherever it is not needed (Settings → General → Membership).
  • Turn off what you don't use. If a site does not rely on the plugin's AJAX/shortcode rendering of non-public content, deactivate the plugin or that specific feature until a fix ships.
  • Virtual patching / WAF. A web application firewall can be set to require authentication for requests to admin-ajax.php carrying the affected action values — or to rate-limit and inspect them — and to block the state-changing action from unauthenticated or low-privilege sessions. Treat this as a stop-gap, not a replacement for the code fix.
  • Watch the indicators. In access logs, look for bursts of admin-ajax.php requests that cycle through many sequential numeric ids, and for admin-ajax.php requests that change plugin options from sessions where you would not expect it. An unexpected change to the plugin's module/settings option is a signal on its own.
  • Protect non-public content. While the plugin is active and unpatched, treat drafts, private posts, and ACF field values as potentially reachable, and keep secrets out of ACF fields on an exposed site.
  • Stay current. Follow the plugin's changelog and apply the security release the moment it ships.

8. The broader lesson

The principle at the bottom of all this is among the oldest in web security, and still among the most broken:

A CSRF token is not an authorization decision. Authentication is not authorization. "The object exists" is not "the caller may see this object."

Each endpoint here fails on one of those three points. A nonce proves a request was intended; it says nothing about whether the requester is allowed. Registering a handler for logged-out visitors (nopriv) commits you to code that is safe for the entire internet, which means it needs its own authorization rather than a token it hands to everyone. And whenever a user-controlled id selects an object (CWE-639), the code has to verify permission on that specific object (CWE-862), not merely that the id resolves to something. Treat every wp_ajax_ / wp_ajax_nopriv_ handler as a public API endpoint, and ask of each one — who may call this, and on which objects? — before the request ever touches the data.

9. Responsible disclosure

This research follows coordinated vulnerability disclosure. The findings were reported privately to the plugin's maintainer, and the full operational proof of concept — the exact request sequences, parameter values, and reproduction commands — is deliberately held back from this public article until a fix is available. We will coordinate the timing of any further technical publication with the vendor.

All testing ran against the public, GPL-licensed plugin on an isolated, localhost-bound lab. No third-party or production systems were accessed, and no non-public data belonging to any third party was collected.

References

Published for defensive and educational purposes. Noroxi LLC conducts authorized security research only; operational exploit detail is withheld until a fix is available.

Exploited this week · every Monday

New KEV entries, fresh PoCs, WordPress triage and what to do this week. No account needed.

Your address is used only for this email and never shared.

Up next

ElementsKit Lite 4.0.7: Unauthenticated Template Disclosure