CVE-2025-67961

WPO365 <= 40.0 - Authenticated (Subscriber+) Server-Side Request Forgery

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

Description

The WPO365 | SEAMLESS WORDPRESS + MICROSOFT INTEGRATION (WPO365 | LOGIN) plugin for WordPress is vulnerable to Server-Side Request Forgery in all versions up to, and including, 40.0. This makes it possible for authenticated attackers, with Subscriber-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<=40.0
PublishedJanuary 21, 2026
Last updatedJanuary 28, 2026
Affected pluginwpo365-login

What Changed in the Fix

Changes introduced in v40.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan focuses on exploiting a Server-Side Request Forgery (SSRF) vulnerability in the **WPO365 | LOGIN** plugin (version <= 40.0). The vulnerability lies in the plugin's "Microsoft Graph Proxy" feature, which allows authenticated users to make arbitrary web requests. ### 1. Vulnerabili…

Show full research plan

This research plan focuses on exploiting a Server-Side Request Forgery (SSRF) vulnerability in the WPO365 | LOGIN plugin (version <= 40.0). The vulnerability lies in the plugin's "Microsoft Graph Proxy" feature, which allows authenticated users to make arbitrary web requests.

1. Vulnerability Summary

The WPO365 plugin provides a REST API proxy to facilitate communication with Microsoft Graph. This proxy endpoint fails to properly validate the destination of the requests, allowing authenticated users (with at least Subscriber-level access) to redirect these requests to arbitrary internal or external URLs. This is a classic SSRF that can be used to scan internal networks, access cloud metadata services (e.g., AWS/GCP metadata), or interact with internal services that rely on IP-based authentication.

2. Attack Vector Analysis

  • Endpoint: /wp-json/wpo365/v1/graph (inferred from documentation and plugin architecture).
  • Method: POST or GET.
  • Vulnerable Parameter: url (inferred).
  • Authentication: Required (Subscriber or higher).
  • Preconditions:
    1. Plugin version <= 40.0 installed.
    2. A valid Subscriber account.
    3. A valid REST API nonce (wp_rest).

3. Code Flow (Inferred)

  1. Registration: The plugin registers a REST route in a class (likely Wpo\Rest\Service) using register_rest_route('wpo365/v1', '/graph', ...).
  2. Permission Check: The permission_callback for this route likely uses is_user_logged_in(), allowing any authenticated user (Subscriber+) to access it.
  3. Proxy Logic: The callback function (e.g., proxy_request) retrieves the url parameter from the WP_REST_Request.
  4. The Sink: The plugin uses wp_remote_get() or wp_remote_post() (or a wrapper around curl) to fetch the content of the provided url without verifying that it belongs to graph.microsoft.com.
  5. Output: The response from the arbitrary URL is returned to the attacker.

4. Nonce Acquisition Strategy

The WordPress REST API requires a nonce for non-GET requests (and often for GET requests when using certain authentication schemes). Since we are attacking an authenticated endpoint, we must obtain the wp_rest nonce.

  1. Login: Use the execution agent to log in as a Subscriber.
  2. Navigation: Navigate to the WordPress Dashboard (/wp-admin/).
  3. Extraction: WordPress automatically localizes the REST API settings into the wpApiSettings JavaScript object.
  4. Execution Agent Command:
    // Use browser_eval to extract the nonce
    const nonce = await browser_eval("window.wpApiSettings?.nonce");
    
    If wpApiSettings is unavailable, the nonce can be found in the page source via regex: _wpnonce["\s:]+([a-f0-9]{10}).

5. Exploitation Strategy

We will use the http_request tool to communicate with the REST API.

  • Target URL: http://<TARGET_HOST>/wp-json/wpo365/v1/graph
  • Payload (External Listener): Use a url parameter pointing to a listener (e.g., Interactsh or a custom server) to confirm the request originates from the WordPress server.
  • Payload (Internal/Metadata): http://169.254.169.254/latest/meta-data/ (AWS) or http://localhost:80 (Internal port scanning).

Example Request:

POST /wp-json/wpo365/v1/graph HTTP/1.1
Host: <TARGET_HOST>
Content-Type: application/json
X-WP-Nonce: <EXTRACTED_NONCE>
Cookie: <SUBSCRIBER_COOKIES>

{
    "url": "http://169.254.169.254/latest/meta-data/",
    "method": "GET"
}

6. Test Data Setup

  1. Install Plugin: Ensure WPO365 version 40.0 is installed.
  2. Create User: Create a user with the Subscriber role.
    wp user create attacker attacker@example.com --role=subscriber --user_pass=password123
    
  3. Environment: Ensure the WordPress server can reach the target of the SSRF (e.g., a test internal service or a mock metadata server).

7. Expected Results

  • Success: The response will contain the HTTP response body of the URL provided in the url parameter. For example, if targeting AWS metadata, the response will show metadata categories.
  • HTTP Status: A successful proxy request will likely return 200 OK with the proxied data.
  • Evidence: The server logs of the target URL (the "sink") will show an incoming request from the WordPress server's IP address.

8. Verification Steps

  1. Check Plugin Version:
    wp plugin get wpo365-login --field=version
    
    Confirm it is 40.0 or lower.
  2. Log Review: After the exploit, check the WordPress server's access logs to verify the POST request to /wp-json/wpo365/v1/graph.
  3. Callback Verification: If using an external listener, confirm that the User-Agent in the received request matches the default WordPress user agent (e.g., WordPress/X.X.X; http://...).

9. Alternative Approaches

  • GET Request: Try passing the payload via a GET request if POST is blocked or requires additional parameters:
    GET /wp-json/wpo365/v1/graph?url=http://internal-service/
  • Header Injection: Check if the proxy allows passing custom headers (e.g., via a headers JSON key), which could be used for more advanced SSRF exploitation (like attacking internal Redis or Memcached instances).
  • Vulnerable File Discovery: If the REST route is not /graph, scan for other routes under the wpo365/v1 namespace using:
    wp rest route list --namespace=wpo365/v1
    

Check if your site is affected.

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