CVE-2026-4338

ActivityPub < 8.0.2 - Unauthenticated Information Epxosure

highExposure of Sensitive Information to an Unauthorized Actor
7.5
CVSS Score
7.5
CVSS Score
high
Severity
8.0.2
Patched in
29d
Time to patch

Description

The ActivityPub plugin for WordPress is vulnerable to Sensitive Information Exposure in all versions up to 8.0.2 (exclusive). This makes it possible for unauthenticated attackers to extract sensitive user or configuration data.

CVSS Vector Breakdown

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

Technical Details

Affected versions<8.0.2
PublishedMarch 18, 2026
Last updatedApril 15, 2026
Affected pluginactivitypub

What Changed in the Fix

Changes introduced in v8.0.2

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-4338 (ActivityPub Information Exposure) ## 1. Vulnerability Summary The ActivityPub plugin for WordPress is vulnerable to **Unauthenticated Sensitive Information Exposure** in versions up to 8.0.2. The vulnerability exists within the plugin's custom REST API e…

Show full research plan

Exploitation Research Plan: CVE-2026-4338 (ActivityPub Information Exposure)

1. Vulnerability Summary

The ActivityPub plugin for WordPress is vulnerable to Unauthenticated Sensitive Information Exposure in versions up to 8.0.2. The vulnerability exists within the plugin's custom REST API endpoints (registered in activitypub.php). Specifically, the controllers responsible for serving ActivityPub objects (like Rest\Post_Controller and Rest\Outbox_Controller) fail to adequately verify post visibility, status, or user permissions before transforming and serving data. This allows an unauthenticated attacker to retrieve content from private posts, password-protected posts, or restricted user profiles by querying the ActivityPub REST API namespace.

2. Attack Vector Analysis

  • Endpoints:
    • Single Post: /wp-json/activitypub/1.0/posts/<id>
    • Actor Outbox: /wp-json/activitypub/1.0/actors/<user_id>/outbox
    • Actor Profile: /wp-json/activitypub/1.0/actors/<user_id>
  • HTTP Method: GET
  • Authentication: Unauthenticated (No cookies or credentials required).
  • Payload: No specific payload is required beyond the ID of the sensitive resource.
  • Preconditions: The ActivityPub plugin must be active. A post must exist that is set to private or has a password.

3. Code Flow

  1. Entry Point: An unauthenticated GET request is made to a route registered in activitypub.php via rest_init(), such as Rest\Post_Controller.
  2. Route Handling: The Rest\Post_Controller::get_item() method (inferred) is invoked.
  3. Data Retrieval: The controller fetches the post using get_post($id).
  4. Vulnerable Sink: The controller fails to call is_post_disabled() (from includes/functions-post.php) or check current_user_can('read_post', $post_id).
  5. Transformation: The post object is passed to Activitypub\Transformer\Post::to_object() (in includes/transformer/class-post.php).
  6. Output: The transformer converts the full post content (including private content) into an ActivityPub Note or Article JSON object, which is then returned to the unauthenticated requester.

4. Nonce Acquisition Strategy

This vulnerability involves unauthenticated information exposure via GET requests to REST API endpoints.

  • Nonce Requirement: In WordPress, REST API GET routes typically do not require a nonce unless they are specifically protected by a permission_callback that checks for one.
  • Bypass: Since the vulnerability is categorized as unauthenticated exposure, the permission_callback is either missing, returns true, or fails to check the user's authorization to view the specific object.
  • Verification: If an initial request returns a 401/403 error, the Rest\Post_Controller may be checked for its permission_callback implementation. However, for "discovery" endpoints in ActivityPub, nonces are rarely used.

5. Exploitation Strategy

Step 1: Discover Post IDs

Identify IDs of posts on the target site. Standard WordPress behavior often leaks these via /wp-json/wp/v2/posts for public posts, but for private posts, an attacker may simply brute-force IDs (e.g., 1 to 100).

Step 2: Extract Private Post Content

Query the ActivityPub post endpoint for a known private post.

  • Request:
    GET /wp-json/activitypub/1.0/posts/<PRIVATE_POST_ID> HTTP/1.1
    Host: TARGET_HOST
    Accept: application/activity+json
    

Step 3: Extract Password-Protected Content

Query the ActivityPub post endpoint for a post that is password-protected.

  • Request:
    GET /wp-json/activitypub/1.0/posts/<PASSWORD_POST_ID> HTTP/1.1
    Host: TARGET_HOST
    Accept: application/activity+json
    

Step 4: Extract Outbox History

Query the author's outbox to see a timeline of activities, which may include summaries or links to content otherwise hidden.

  • Request:
    GET /wp-json/activitypub/1.0/actors/<USER_ID>/outbox HTTP/1.1
    Host: TARGET_HOST
    Accept: application/activity+json
    

6. Test Data Setup

To verify the vulnerability, the following data must be prepared in the test environment using WP-CLI:

  1. Create a Private Post:

    wp post create --post_type=post --post_title="Sensitive Internal Memo" --post_content="This is secret content only for admins." --post_status=private --post_author=1
    

    Note the resulting ID (e.g., ID 5).

  2. Create a Password-Protected Post:

    wp post create --post_type=post --post_title="Protected Strategy" --post_content="The password is required to see this strategy." --post_status=publish --post_password="password123" --post_author=1
    

    Note the resulting ID (e.g., ID 6).

  3. Ensure Plugin is Configured:
    Enable the ActivityPub plugin if not already active.

    wp plugin activate activitypub
    

7. Expected Results

  • Success: The REST API response for the private post (ID 5) returns a JSON object with type: "Note" or type: "Article" containing the string "This is secret content only for admins." in the content field.
  • Success: The REST API response for the password-protected post (ID 6) returns the post content without requiring the password.
  • Failure (Patched): The REST API returns a 403 Forbidden, 401 Unauthorized, or a 404 Not Found error, or the JSON object contains a generic "Private" message instead of the content.

8. Verification Steps

  1. Direct HTTP Check: Use the http_request tool to perform the GET requests described in Section 5.
  2. Content Verification: Parse the JSON response body and search for the specific strings created in the Test Data Setup.
  3. Log Check: Check the WordPress error logs or access logs to confirm the requests were handled by the ActivityPub REST handlers.

9. Alternative Approaches

  • Content Negotiation: If the /wp-json/activitypub/1.0/posts/ route is not directly accessible, try requesting the standard permalink of a private post with the header Accept: application/activity+json. The plugin's includes/functions-request.php logic might hijack the request and serve the JSON representation.
  • User Discovery: Query /wp-json/activitypub/1.0/actors/1 to see if sensitive metadata (like the user's slug or internal IDs) is exposed for users who have not opted into federation.
Research Findings
Static analysis — not yet PoC-verified

Summary

The ActivityPub plugin for WordPress exposes sensitive content from non-public posts (drafts, pending, scheduled, or private) via its custom REST API endpoints. Unauthenticated attackers can bypass standard WordPress visibility restrictions and password protections by accessing the ActivityPub JSON representation of restricted resources.

Vulnerable Code

// includes/functions-post.php lines 36-42
	if (
		$is_local_or_private ||
		! \post_type_supports( $post->post_type, 'activitypub' ) ||
		'private' === $post->post_status ||
		! empty( $post->post_password )
	) {
		$disabled = true;
	}

---

// includes/transformer/class-post.php line 596
		// Remove Content from drafts.
		if ( ! $this->is_preview() && 'draft' === \get_post_status( $this->item ) ) {
			$this->content = \__( '(This post is being modified)', 'activitypub' );

			return $this->content;
		}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/activitypub/8.0.1/includes/functions-post.php /home/deploy/wp-safety.org/data/plugin-versions/activitypub/8.0.2/includes/functions-post.php
--- /home/deploy/wp-safety.org/data/plugin-versions/activitypub/8.0.1/includes/functions-post.php	2026-02-05 09:09:42.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/activitypub/8.0.2/includes/functions-post.php	2026-03-17 11:34:48.000000000 +0000
@@ -29,10 +29,14 @@
 	$visibility          = \get_post_meta( $post->ID, 'activitypub_content_visibility', true );
 	$is_local_or_private = in_array( $visibility, array( ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE ), true );
 
+	// Only 'publish' is public. 'inherit' is allowed only for attachments.
+	$is_public_status = 'publish' === $post->post_status ||
+		( 'inherit' === $post->post_status && 'attachment' === $post->post_type );
+
 	if (
 		$is_local_or_private ||
 		! \post_type_supports( $post->post_type, 'activitypub' ) ||
-		'private' === $post->post_status ||
+		! $is_public_status ||
 		! empty( $post->post_password )
 	) {
 		$disabled = true;
@@ -40,14 +44,17 @@
 
 	/*
 	 * Check for posts that need special handling.
-	 * Federated posts changed to local/private need Delete activity.
+	 * Federated posts changed to local/private or non-public status need Delete activity.
 	 * Deleted posts restored to public need Create activity.
 	 */
 	$object_state = get_wp_object_state( $post );
 
 	if (
 		ACTIVITYPUB_OBJECT_STATE_DELETED === $object_state ||
-		( $is_local_or_private && ACTIVITYPUB_OBJECT_STATE_FEDERATED === $object_state )
+		(
+			ACTIVITYPUB_OBJECT_STATE_FEDERATED === $object_state &&
+			( $is_local_or_private || ! $is_public_status )
+		)
 	) {
 		$disabled = false;
 	}

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/activitypub/8.0.1/includes/transformer/class-post.php /home/deploy/wp-safety.org/data/plugin-versions/activitypub/8.0.2/includes/transformer/class-post.php
--- /home/deploy/wp-safety.org/data/plugin-versions/activitypub/8.0.1/includes/transformer/class-post.php	2026-03-05 08:08:28.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/activitypub/8.0.2/includes/transformer/class-post.php	2026-03-17 11:34:48.000000000 +0000
@@ -359,10 +359,10 @@
 		}
 
 		/*
-		 * Remove attachments from the Fediverse if a post was federated and then set back to draft.
+		 * Remove attachments from the Fediverse if a post was federated and then unpublished.
 		 * Except in preview mode, where we want to show attachments.
 		 */
-		if ( ! $this->is_preview() && 'draft' === \get_post_status( $this->item ) ) {
+		if ( ! $this->is_preview() && 'publish' !== \get_post_status( $this->item ) ) {
 			$this->attachment = array();
 
 			return $this->attachment;
@@ -543,8 +543,8 @@
 			return $this->summary;
 		}
 
-		// Remove Teaser from drafts.
-		if ( ! $this->is_preview() && 'draft' === \get_post_status( $this->item ) ) {
+		// Remove Teaser from unpublished posts.
+		if ( ! $this->is_preview() && 'publish' !== \get_post_status( $this->item ) ) {
 			$this->summary = \__( '(This post is being modified)', 'activitypub' );
 
 			return $this->summary;
@@ -593,8 +593,8 @@
 			return $this->content;
 		}
 
-		// Remove Content from drafts.
-		if ( ! $this->is_preview() && 'draft' === \get_post_status( $this->item ) ) {
+		// Remove Content from unpublished posts.
+		if ( ! $this->is_preview() && 'publish' !== \get_post_status( $this->item ) ) {
 			$this->content = \__( '(This post is being modified)', 'activitypub' );
 
 			return $this->content;

Exploit Outline

The exploit targets the ActivityPub REST API namespace (default: `/wp-json/activitypub/1.0/`). An unauthenticated attacker sends a GET request to either the post endpoint (`/posts/<ID>`) or the user's outbox endpoint (`/actors/<USER_ID>/outbox`). Because the plugin's internal visibility check only explicitly blocked 'private' status and 'draft' content in the transformer, posts with other non-public statuses (like 'pending' or 'future') or password-protected posts were served in full. By setting the `Accept` header to `application/activity+json`, the attacker receives the JSON representation of the sensitive post, including its full content and attachments.

Check if your site is affected.

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