CVE-2026-32448

Podlove Podcast Publisher <= 4.3.3 - Authenticated (Contributor+) Stored Cross-Site Scripting

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

Description

The Podlove Podcast Publisher plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 4.3.3 due to insufficient input sanitization and output escaping. 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<=4.3.3
PublishedMarch 8, 2026
Last updatedApril 15, 2026

What Changed in the Fix

Changes introduced in v4.3.4

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan targets **CVE-2026-32448**, a stored Cross-Site Scripting (XSS) vulnerability in the Podlove Podcast Publisher plugin. The vulnerability allows authenticated users with Contributor-level permissions or higher to inject malicious scripts into podcast episode metadata, which is subs…

Show full research plan

This research plan targets CVE-2026-32448, a stored Cross-Site Scripting (XSS) vulnerability in the Podlove Podcast Publisher plugin. The vulnerability allows authenticated users with Contributor-level permissions or higher to inject malicious scripts into podcast episode metadata, which is subsequently rendered unsanitized on the frontend.

1. Vulnerability Summary

The Podlove Podcast Publisher plugin manages podcast episodes as a Custom Post Type (podcast). When saving an episode, the plugin stores metadata (such as subtitles, summaries, and episode numbers) in a custom database table (wp_podlove_episode). Versions up to 4.3.3 fail to properly sanitize these inputs upon saving and fail to escape them upon output in frontend templates or shortcodes. This allows a Contributor to inject a payload into an episode field that executes in the context of any user (including Administrators) viewing that episode.

2. Attack Vector Analysis

  • Vulnerable Endpoint: wp-admin/post.php (via the editpost action).
  • Vulnerable Parameter: podlove_episode[subtitle] (and potentially podlove_episode[summary]).
  • Authentication Required: Contributor+ (any user capable of creating/editing a podcast post type).
  • Preconditions: The plugin must be active, which registers the podcast post type and the associated meta boxes.

3. Code Flow

  1. Entry Point: A Contributor submits a POST request to wp-admin/post.php to save or update an episode.
  2. Processing: The plugin hooks into save_post (registered in lib/episode.php, though the file is partially inferred, the behavior is standard for the Model\Episode logic seen in includes/setup.php).
  3. Storage: The Model\Episode class (referenced in includes/setup.php's podlove_setup_database_tables()) extracts data from $_POST['podlove_episode'] and saves it to the wp_podlove_episode table using a save() method similar to the one seen in lib/model/podcast.php.
  4. Sink: When a user views the episode on the frontend, the default template (defined in podlove_setup_default_template in includes/setup.php) or the [podlove-episode-subtitle] shortcode retrieves the data.
  5. Rendering: The template engine (Twig-based) or the shortcode handler outputs the subtitle field directly without calling esc_html() or applying appropriate Twig escaping filters.

4. Nonce Acquisition Strategy

To save post metadata, the agent must obtain the standard WordPress post nonce and the Podlove-specific security nonce used for episode metadata.

  1. Identify Post ID: Create a draft episode or edit an existing one to get a valid post_ID.
  2. Navigate: Use browser_navigate to wp-admin/post-new.php?post_type=podcast.
  3. Extract Nonces:
    • WordPress Post Nonce: Located in the #_wpnonce hidden input.
    • Podlove Nonce: Podlove typically uses a nonce field within its meta box. Look for an input named _podlove_nonce or similar.
    • JS Strategy:
      // Execute in browser_eval
      const wp_nonce = document.querySelector('#_wpnonce')?.value;
      const podlove_nonce = document.querySelector('input[name*="podlove_nonce"]')?.value;
      const post_id = document.querySelector('#post_ID')?.value;
      return { wp_nonce, podlove_nonce, post_id };
      

5. Exploitation Strategy

The goal is to inject a payload into the subtitle field of a podcast episode.

Step-by-Step Plan:

  1. Setup: Ensure a Contributor user exists.
  2. Capture Context: Log in as Contributor and navigate to the "Add New Episode" page.
  3. Submit Payload: Send a POST request to wp-admin/post.php mimicking the form submission.
    • URL: https://<target>/wp-admin/post.php
    • Headers: Content-Type: application/x-www-form-urlencoded
    • Body Parameters:
      • action: editpost
      • post_ID: <post_id_from_step_4>
      • _wpnonce: <wp_nonce_from_step_4>
      • post_type: podcast
      • post_title: Exploit Episode
      • podlove_episode[subtitle]: "><script>alert(document.domain)</script>
      • podlove_episode[summary]: Malicious Summary
      • (Include other required Podlove fields if necessary, e.g., podlove_episode[number])
  4. Trigger: Navigate to the permalink of the newly created episode (e.g., /?podcast=exploit-episode).

6. Test Data Setup

  1. Plugin Setup: Ensure Podlove is installed and the setup wizard is completed (defaulting settings is fine).
  2. User: Create a user with the contributor role.
  3. Enable Modules: Ensure the podlove_web_player is active (it is a default module as per podlove_setup_modules() in includes/setup.php), as the player often renders episode metadata.

7. Expected Results

  • The POST request should return a 302 Redirect to the post edit page.
  • Upon navigating to the episode's frontend page, a JavaScript alert box displaying the document domain should appear.
  • Inspecting the HTML source of the frontend page should show:
    ...<span class="podlove-subtitle">"><script>alert(document.domain)</script></span>... (or similar depending on the active template).

8. Verification Steps

  1. Check Database: Use WP-CLI to verify the payload is stored in the custom table.
    wp db query "SELECT subtitle FROM wp_podlove_episode ORDER BY id DESC LIMIT 1;"
    
  2. Verify Rendering: Perform an unauthenticated http_request to the episode URL and grep for the script.
    # Use the tool to fetch the frontend page
    # Search for "<script>alert"
    

9. Alternative Approaches

  • Payload Location: If subtitle is sanitized, attempt injection in the podlove_episode[summary] field or the podlove_episode[number] field (if it isn't cast to an integer).
  • Template Injection: If the site uses custom Podlove templates, navigate to Podlove > Templates (if the Contributor has podlove_read_templates capability) and try to inject into the template content itself.
  • Shortcode Vector: If the frontend doesn't show the subtitle by default, create a regular post as Contributor and use the shortcode: [podlove-episode-subtitle post_id="<ID>"]. If the shortcode is vulnerable, the XSS will fire when that post is viewed.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Podlove Podcast Publisher plugin for WordPress is vulnerable to Stored Cross-Site Scripting (XSS) in versions up to and including 4.3.3. This occurs because the plugin fails to sufficiently sanitize episode metadata and escape user-supplied attributes in shortcodes, allowing authenticated users with Contributor-level access to inject arbitrary scripts that execute when other users view the content.

Vulnerable Code

// lib/shortcodes.php around line 102
return $cache->cache_for($cache_key, function () use ($template_id, $attributes) {
    if (!$template = Model\Template::find_one_by_title_with_fallback($template_id)) {
        return sprintf(__('Podlove Error: Whoops, there is no template with id "%s"', 'podlove-podcasting-plugin-for-wordpress'), $template_id);
    }

--- 

// lib/model/podcast.php line 147
public function full_title()
{
    $t = $this->title;

    if ($this->subtitle) {
        $t = $t.' - '.$this->subtitle;
    }

    return $t;
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/podlove-podcasting-plugin-for-wordpress/4.3.3/lib/shortcodes.php /home/deploy/wp-safety.org/data/plugin-versions/podlove-podcasting-plugin-for-wordpress/4.3.4/lib/shortcodes.php
--- /home/deploy/wp-safety.org/data/plugin-versions/podlove-podcasting-plugin-for-wordpress/4.3.3/lib/shortcodes.php	2020-09-08 18:35:06.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/podlove-podcasting-plugin-for-wordpress/4.3.4/lib/shortcodes.php	2026-02-20 12:23:36.000000000 +0000
@@ -100,7 +100,8 @@
 
     return $cache->cache_for($cache_key, function () use ($template_id, $attributes) {
         if (!$template = Model\Template::find_one_by_title_with_fallback($template_id)) {
-            return sprintf(__('Podlove Error: Whoops, there is no template with id "%s"', 'podlove-podcasting-plugin-for-wordpress'), $template_id);
+            $safe_template_id = esc_html($template_id);
+            return sprintf(__('Podlove Error: Whoops, there is no template with id "%s"', 'podlove-podcasting-plugin-for-wordpress'), $safe_template_id);
         }
 
         $html = apply_filters('podlove_template_raw', $template->title, $attributes);

Exploit Outline

1. Authenticate to the WordPress dashboard as a user with Contributor role or higher. 2. Navigate to the podcast episode editor (wp-admin/post-new.php?post_type=podcast or post.php). 3. Inject a JavaScript payload into episode metadata fields, specifically the 'subtitle' parameter (e.g., podlove_episode[subtitle]="><script>alert(document.domain)</script>"). 4. Alternatively, create a post and include a Podlove shortcode that uses a template ID attribute containing a malicious payload, such as [podlove-template id="<script>alert(1)</script>"]. 5. Save the post or episode to store the payload in the database. 6. The script will execute in the browser context of any user (including administrators) who visits the frontend page for that episode or the post containing the malicious shortcode.

Check if your site is affected.

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