CVE-2025-14726

Widgets for Social Photo Feed <= 1.8 - Missing Authentication to Unauthenticated Plugin Settings Access/Update via trustindex_feed_hook_instagram REST API endpoints

mediumExposure of Sensitive Information to an Unauthorized Actor
6.5
CVSS Score
6.5
CVSS Score
medium
Severity
1.8.1
Patched in
1d
Time to patch

Description

The Widgets for Social Photo Feed plugin for WordPress is vulnerable to unauthorized access of data and modification of data due to a missing capability check on the '/trustindex_feed_hook_instagram/troubleshooting' and '/trustindex_feed_hook_instagram/submit-data' REST API endpoints in all versions up to, and including, 1.8. This makes it possible for unauthenticated attackers to access and update plugin settings.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=1.8
PublishedMay 1, 2026
Last updatedMay 2, 2026

What Changed in the Fix

Changes introduced in v1.8.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This analysis details a vulnerability in the **Widgets for Social Photo Feed** plugin (<= 1.8), where several REST API endpoints lack proper capability checks, allowing unauthenticated attackers to access sensitive information and modify plugin settings. ### 1. Vulnerability Summary The plugin regi…

Show full research plan

This analysis details a vulnerability in the Widgets for Social Photo Feed plugin (<= 1.8), where several REST API endpoints lack proper capability checks, allowing unauthenticated attackers to access sensitive information and modify plugin settings.

1. Vulnerability Summary

The plugin registers a REST API namespace trustindex_feed_hook_instagram with several endpoints. While these endpoints attempt a custom authentication check via a function getAuthError(), the plugin also provides a public endpoint /get-token that generates and returns the very nonce required to pass this check. Furthermore, the permission_callback for all these routes is set to __return_true, effectively bypassing WordPress's built-in REST API authorization.

2. Attack Vector Analysis

  • Endpoints:
    • GET /wp-json/trustindex_feed_hook_instagram/get-token (Used to obtain the nonce)
    • GET /wp-json/trustindex_feed_hook_instagram/troubleshooting (Information Exposure)
    • POST /wp-json/trustindex_feed_hook_instagram/submit-data (Settings Modification)
    • GET /wp-json/trustindex_feed_hook_instagram/refresh-data (Data Exposure)
  • Namespace: trustindex_feed_hook_instagram (dynamically generated via getWebhookAction() in trustindex-feed-plugin.class.php)
  • Authentication: None required (unauthenticated).
  • Preconditions: The plugin must be active.

3. Code Flow

  1. Registration: In social-photo-feed-widget.php (lines 127-183), the plugin uses register_rest_route to define hooks.
  2. Auth Bypass: Each route defines 'permission_callback' => '__return_true'.
  3. The "Key" to the Vault: The /get-token endpoint (lines 128-135) calls wp_create_nonce('admin_action_nonce') and returns it in a JSON response.
  4. The Vulnerable Sink: The /submit-data endpoint (lines 154-183) calls getAuthError($request). Based on the /get-token endpoint, getAuthError expects a nonce parameter matching admin_action_nonce.
  5. Execution: Once the nonce check is passed, /submit-data takes the data parameter, unsanitizes it (via wp_unslash), passes it to sanitizeJsonData, and then calls saveConnectedSource($source). This function updates the WordPress option trustindex-instagram-source.

4. Nonce Acquisition Strategy

Unlike typical WordPress vulnerabilities requiring complex extraction, this plugin provides the nonce directly via its own API.

  1. Action String: admin_action_nonce
  2. Acquisition Method: Send a simple GET request to the REST API.
  3. HTTP Request:
    GET /wp-json/trustindex_feed_hook_instagram/get-token HTTP/1.1
    Host: localhost
    
  4. Expected Response: {"nonce":"[10_digit_nonce]"}

5. Exploitation Strategy

Step 1: Obtain the Nonce

Use the http_request tool to fetch the nonce.

Step 2: Information Disclosure (Troubleshooting)

Access the troubleshooting data which contains system environment details.

  • Method: GET
  • URL: /wp-json/trustindex_feed_hook_instagram/troubleshooting
  • Query Param: nonce=[obtained_nonce]
  • Expected Result: A <pre> block containing server info, versions, and internal plugin state.

Step 3: Plugin Settings Modification

Modify the "source" configuration to prove modification capability.

  • Method: POST
  • URL: /wp-json/trustindex_feed_hook_instagram/submit-data
  • Body (URL-encoded):
    nonce=[obtained_nonce]&data={"name":"PwnedSource","user_id":"12345","active":true}
    
  • Content-Type: application/x-www-form-urlencoded

6. Test Data Setup

  1. Install and activate Widgets for Social Photo Feed version 1.7.9 or 1.8.
  2. No specific settings are required as the vulnerability exists in the default state upon activation.

7. Expected Results

  • Success (Get Token): HTTP 200 with a JSON body containing a 10-character alphanumeric nonce.
  • Success (Troubleshooting): HTTP 200 with a text/html response displaying internal server diagnostics.
  • Success (Submit Data): HTTP 200 with a JSON body containing a token (the public-id option value).

8. Verification Steps

After the POST request, verify the database state using WP-CLI:

# Check if the 'source' option was updated
wp option get trustindex-instagram-source --format=json

The output should contain "name":"PwnedSource".

9. Alternative Approaches

If getAuthError does not accept the nonce via query parameters, it may expect it in an HTTP header:

  • Alternative Header: X-WP-Nonce: [obtained_nonce] or X-Trustindex-Auth: [obtained_nonce]

If the /submit-data endpoint requires specific JSON structure to avoid early return, use the structure found in getConnectedSource():

{
    "name": "{\"val\":\"Exploit\"}",
    "user_id": "999",
    "access_token": "malicious_token"
}
Research Findings
Static analysis — not yet PoC-verified

Summary

The Widgets for Social Photo Feed plugin for WordPress contains several REST API endpoints that are unprotected by capability checks and rely on a nonce that is exposed via a public endpoint. This allows unauthenticated attackers to retrieve internal system diagnostics and modify plugin settings, including the connected Instagram source configuration.

Vulnerable Code

// social-photo-feed-widget.php lines 127-148
add_action('rest_api_init', function () use ($pluginManagerInstance) {
register_rest_route($pluginManagerInstance->getWebhookAction(), '/get-token', [
'methods' => 'GET',
'callback' => function () {
return rest_ensure_response([
'nonce' => wp_create_nonce('admin_action_nonce'),
]);
},
'permission_callback' => '__return_true',
]);
register_rest_route($pluginManagerInstance->getWebhookAction(), '/troubleshooting', [
'methods' => 'GET',
'callback' => function (WP_REST_Request $request) use ($pluginManagerInstance) {
$authError = $pluginManagerInstance->getAuthError($request);
if (null !== $authError) {
return $authError;
}
header('Content-Type: text/html; charset=UTF-8');
global $wpdb, $wp_version;
echo '<pre>';
include $pluginManagerInstance->getPluginDir() . 'include' . DIRECTORY_SEPARATOR . 'troubleshooting.php';
echo '</pre>';
exit;
},
'permission_callback' => '__return_true',
]);

---

// social-photo-feed-widget.php lines 154-171
register_rest_route($pluginManagerInstance->getWebhookAction(), '/submit-data', [
'methods' => 'POST',
'callback' => function (WP_REST_Request $request) use ($pluginManagerInstance) {
$authError = $pluginManagerInstance->getAuthError($request);
if (null !== $authError) {
return $authError;
}
$oldSource = $pluginManagerInstance->getConnectedSource();
/*
This function ensures that each element of the JSON object is sanitized individually using standard WordPress sanitization functions
*/
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$source = $pluginManagerInstance->sanitizeJsonData(wp_unslash($request->get_param('data')), false);

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/social-photo-feed-widget/1.7.9/trustindex-feed-plugin.class.php /home/deploy/wp-safety.org/data/plugin-versions/social-photo-feed-widget/1.8.1/trustindex-feed-plugin.class.php
--- /home/deploy/wp-safety.org/data/plugin-versions/social-photo-feed-widget/1.7.9/trustindex-feed-plugin.class.php	2026-02-26 08:47:58.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/social-photo-feed-widget/1.8.1/trustindex-feed-plugin.class.php	2026-04-23 09:53:30.000000000 +0000
@@ -122,6 +122,25 @@
 
 public function activate()
 {
+$requestBody = [
+'platform' => 'Instagram',
+'website' => get_option('siteurl'),
+];
+$response = wp_remote_post('https://admin.trustindex.io/new/wordpress-feed/register', [
+'headers' => [
+'Content-Type' => 'application/x-www-form-urlencoded',
+'ti-secure' => hash_hmac('sha256', http_build_query($requestBody), '80ce0e06b31b34794f5088d4875480f1'),
+],
+'body' => $requestBody,
+'timeout' => '30',
+'sslverify' => false,
+]);
+if (is_wp_error($response)) {
+update_option($this->getOptionName('public-id'), $response->get_error_message(), false);
+return;
+}
+$data = json_decode(wp_remote_retrieve_body($response), true);
+update_option($this->getOptionName('public-id'), $data['public-id'] ?? $data['error'], false);
 include $this->getPluginDir() . 'include' . DIRECTORY_SEPARATOR . 'activate.php';
 if (!$this->getNotificationParam('rate-us', 'hidden', false) && $this->getNotificationParam('rate-us', 'active', true)) {
 $this->setNotificationParam('rate-us', 'active', true);
@@ -5515,8 +5535,12 @@
 if (1800 < (time() - $timestamp)) {
 return new WP_Error('expired', 'Request expired', ['status' => 401]);
 }
+$publicId = get_option($this->getOptionName('public-id'));
+if (!$publicId) {
+return new WP_Error('missing_public_id', 'Public ID is missing', ['status' => 400]);
+}
 $body = $request->get_body();
-$expected = hash_hmac('sha256', $body.$timestamp, get_option($this->getOptionName('public-id'), $request->get_param('data')['public_id']));
+$expected = hash_hmac('sha256', $body.$timestamp, $publicId);
 if (!hash_equals($expected, $signature)) {
 return new WP_Error('invalid_signature', 'Signature mismatch', ['status' => 403]);
 }

Exploit Outline

1. **Retrieve Valid Nonce**: Perform an unauthenticated GET request to `/wp-json/trustindex_feed_hook_instagram/get-token`. This endpoint returns a valid nonce for the `admin_action_nonce` action. 2. **Identify Target Endpoint**: Choose either `/troubleshooting` (for information disclosure) or `/submit-data` (for modification). 3. **Bypass Internal Auth**: Use the retrieved nonce in the `nonce` parameter. The `permission_callback` for the REST routes is set to `__return_true`, so the nonce is the only check protecting the functionality. 4. **Execute Information Disclosure**: Send a GET request to `/wp-json/trustindex_feed_hook_instagram/troubleshooting?nonce=[NONCE]` to receive a full diagnostic dump of the site. 5. **Execute Configuration Update**: Send a POST request to `/wp-json/trustindex_feed_hook_instagram/submit-data` with the `nonce` parameter and a `data` parameter containing a JSON-encoded payload representing the new source configuration. This payload will be processed and used to update the `trustindex-instagram-source` option.

Check if your site is affected.

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