A podcast plugin left the studio door unlocked and forgot to check ID at the mixing board
The Podlove Podcast Publisher plugin exposes a move_as_original_file routine in lib/model/image.php that accepts uploads without validating file type, extension, or MIME. An unauthenticated HTTP request can drop a .php webshell into a web-accessible directory, giving the attacker code execution as the webserver user on any WordPress instance running the plugin. Everything at and below version 4.2.6 is affected; 4.2.7 ships the fix.
The vendor 9.8 label is *technically* correct for the shape of the bug — no auth, no interaction, RCE — but it ignores population. Wordfence/WPScan-class CVSS scoring rewards raw ergonomics, not exposure. With ~4,000 active installs on a niche podcasting plugin, this is a HIGH by any real-world blast-radius measure, not a fleet-melting CRITICAL like a WooCommerce or Elementor bug of the same shape.
4 steps from start to impact.
Fingerprint plugin presence
/wp-content/plugins/podlove-podcasting-plugin-for-wordpress/ or the plugin's readme.txt to confirm install and version. Mass scanners (wpscan, nuclei with community WordPress templates) already ship WordPress plugin enumeration.- Target WordPress site reachable on 80/443
- Plugin directory not blocked at edge
- Only ~4,000 sites globally run this plugin — targeted scanning yields near-zero hit rate
- Some hosts hide plugin paths via WAF or
wp_hide_plugin-style hardening
nuclei template http/wordpress/plugins/podlove-podcasting-plugin-for-wordpress.yaml catches version disclosure. WPScan CLI catches version and known-CVE.Hit the unauthenticated upload endpoint
move_as_original_file. No nonce, no capability check, no MIME sniff — the file lands on disk with attacker-controlled extension. A generic curl -F with a PHP payload is enough; no custom exploit framework required.- Vulnerable Podlove ≤ 4.2.6 installed and active
- PHP execution allowed in the upload target directory
- Managed WP hosts (WP Engine, Kinsta, Pantheon) block PHP execution in
wp-content/uploadsby default - ModSecurity CRS rule 932100 catches PHP tags in multipart body
.php filename or <?php payload. Wordfence Premium ships a signature within days of a public PoC.Fetch the webshell
www-data or nobody, sometimes the site's own UID under shared hosting.- Uploaded file lives inside document root
.htaccessor Nginx location block does not deny.phpunder uploads
- Hardened WP configs deny
.phpinwp-content/uploads - Read-only container filesystems (Roots Bedrock, Docker+ephemeral) reject the write
sh, curl, wget, or outbound connections to unknown IPs.Loot and pivot
wp-config.php (DB creds, AUTH_KEYS), dumps the users table, plants a persistence backdoor in mu-plugins/, and — if DB creds are shared — pivots to the MySQL/MariaDB tier. On multi-tenant hosting, escalation stops at the site boundary; on a self-hosted VPS, they own the box.- DB reachable from web tier
- No FIM/OSSEC watching
wp-contentandwp-config.php
- Site isolation on managed hosts contains blast to one tenant
- chrooted/jailed PHP-FPM pools limit horizontal reach
wp-cli --skip-plugins verify-checksums) catch modified core/plugin files.wp plugin update podlove-podcasting-plugin-for-wordpress via WP-CLI takes seconds and requires no schema migration. Deploy within 30 days per the noisgate mitigation SLA for HIGH; ideally same-day on public-facing WP.wp-content/uploads/ — Drop an .htaccess with <FilesMatch "\.php$"> Require all denied </FilesMatch> or an Nginx location ~ ^/wp-content/uploads/.*\.php$ { deny all; } block. This kills the RCE step even if the upload lands. Standard WP hardening — deploy fleet-wide within 30 days regardless of this CVE.wp-content/plugins/ and wp-config.php — Use Wordfence, Sucuri, or Tripwire to alert on new .php files in unexpected paths. Catches successful exploitation and post-exploit persistence (mu-plugins backdoors). Standing control.- Requiring a login for
/wp-admin/— the vulnerable endpoint is reachable pre-auth; hiding wp-login.php does nothing. - Fail2ban on brute-force patterns — this is a single-request exploit, not a brute-force one; rate-limiting login attempts is unrelated.
- MFA on WordPress admins — no admin session is required to trigger the upload.
- Cloudflare bot mitigation on default settings — a plain
curl -Fwith a benign-looking filename slips past most bot rules unless you specifically tune for.phpuploads.
The supporting signals.
| In-the-wild status | No known active exploitation as of 2026-07-15. Not on CISA KEV. No public campaign reporting from Wordfence Threat Intel, Patchstack, or Sucuri. |
|---|---|
| Public PoC | No weaponized public PoC in Exploit-DB or GitHub as of assessment. ZeroPath and Patchstack advisories describe the vulnerable function (move_as_original_file in lib/model/image.php:465) — enough for a competent attacker to derive one. |
| EPSS | 0.882% — bottom quartile. Reflects both low install base and absent public exploit. |
| KEV status | Not listed. Would need active mass exploitation to be added; niche install base makes this unlikely. |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H = 9.8. Network, no auth, no UI — vendor is scoring the *shape* of the bug, not population. |
| Affected versions | Podlove Podcast Publisher ≤ 4.2.6 (all prior versions). |
| Fixed version | 4.2.7 — released with hardened image download-cache handling. No distro backports; plugin ships via WordPress.org and GitHub only. |
| Exposure / footprint | ~4,000 active installs per WordPress.org plugin directory. Not enumerated by GreyNoise (no known exploit traffic tag). Shodan/Censys count of exposed WP sites is millions, but this specific plugin footprint is a rounding error. |
| Disclosure date | 2025-09-23 via Patchstack and Wordfence Intelligence. |
| Reporter | Credited via Wordfence/Patchstack coordinated disclosure programs. |
Sources.
Why this verdict
- Unauth RCE with zero friction on affected sites: no capability check, no nonce, no MIME validation — a single
curlPOST wins. On any host actually running Podlove ≤ 4.2.6 with PHP execution permitted in uploads, this is a full site takeover. - Installed base narrows the fleet math: ~4,000 sites globally versus millions for a plugin like Elementor. In a 10,000-host enterprise, you are unlikely to have more than one or two instances, typically on a marketing/podcasting subsite — not the identity plane.
- Role multiplier — WordPress marketing/podcast tier: compromise ends at the site boundary on managed hosting or at the VPS boundary on self-hosted. This plugin is not deployed on identity providers, hypervisors, CI runners, or backup servers. Blast radius is site-scale, not fleet-scale. Verdict floor stays at HIGH, does not reach CRITICAL.
- No KEV, low EPSS, no observed campaigns: attacker interest tracks target density, and the target density here is low. But because the bug is easy, expect opportunistic scanning once a PoC is published.
- Compensating controls are cheap: deny
.phpexecution underwp-content/uploads, put the site behind Wordfence or Patchstack, or simply update — no complex regression testing required for a plugin-level bump.
Why not higher?
Not CRITICAL because installed base is negligible (~4K sites) and the affected component does not sit on an identity, hypervisor, backup, edge, or CI role where compromise cascades. Blast radius stops at a single WordPress tenant. No active exploitation, no KEV, EPSS well under 1%.
Why not lower?
Not MEDIUM because for the ~4,000 sites actually running Podlove ≤ 4.2.6, this is an *unauthenticated* pre-auth RCE with a trivial one-request exploit path. Any site running it gets fully owned; there is no meaningful workflow friction. MEDIUM would imply the attacker needs auth or a rare configuration — neither is true here.
Crowdsourced verification payload.
Run from the WordPress host as any user with read access to the webroot, or from an auditor workstation via SSH. Pass the WordPress install path as the first argument, e.g. ./check-cve-2025-10147.sh /var/www/html. No elevated privileges required — this is a read-only version check.
#!/usr/bin/env bash
# noisgate verifier — CVE-2025-10147 (Podlove Podcast Publisher <= 4.2.6)
# Usage: ./check-cve-2025-10147.sh /path/to/wordpress
# Exit codes: 0 = PATCHED / NOT INSTALLED, 1 = VULNERABLE, 2 = UNKNOWN
set -u
WP_ROOT="${1:-/var/www/html}"
PLUGIN_DIR="$WP_ROOT/wp-content/plugins/podlove-podcasting-plugin-for-wordpress"
FIXED_VERSION="4.2.7"
if [[ ! -d "$WP_ROOT" ]]; then
echo "UNKNOWN: WordPress root '$WP_ROOT' not found"
exit 2
fi
if [[ ! -d "$PLUGIN_DIR" ]]; then
echo "PATCHED: Podlove Podcast Publisher not installed at $WP_ROOT"
exit 0
fi
# Extract Version: header from main plugin file
PLUGIN_FILE="$PLUGIN_DIR/podlove.php"
if [[ ! -f "$PLUGIN_FILE" ]]; then
# fall back to any php file with a plugin header
PLUGIN_FILE=$(grep -rl "Plugin Name:.*Podlove" "$PLUGIN_DIR" 2>/dev/null | head -n1)
fi
if [[ -z "${PLUGIN_FILE:-}" || ! -f "$PLUGIN_FILE" ]]; then
echo "UNKNOWN: could not locate Podlove plugin header file"
exit 2
fi
VERSION=$(grep -iE '^\s*\*?\s*Version:' "$PLUGIN_FILE" | head -n1 | sed -E 's/.*Version:\s*//I' | tr -d '\r' | awk '{print $1}')
if [[ -z "$VERSION" ]]; then
echo "UNKNOWN: version header not readable in $PLUGIN_FILE"
exit 2
fi
# Version comparison via sort -V
LOWEST=$(printf '%s\n%s\n' "$VERSION" "$FIXED_VERSION" | sort -V | head -n1)
if [[ "$VERSION" == "$FIXED_VERSION" || "$LOWEST" == "$FIXED_VERSION" ]]; then
echo "PATCHED: Podlove Podcast Publisher $VERSION (>= $FIXED_VERSION)"
exit 0
else
echo "VULNERABLE: Podlove Podcast Publisher $VERSION (< $FIXED_VERSION) — CVE-2025-10147"
exit 1
fi