# Technical Review: CVE-2026-48866 PoC ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ CVE-2026-48866 │ │ Gravity Forms <= 2.10.0.1 — Path Traversal Arbitrary File Deletion │ │ │ │ CVSS 3.1: 9.6 CRITICAL AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H │ │ CWE: CWE-22 (Improper Limitation of a Pathname to a Restricted │ │ Directory — Path Traversal) │ │ CNA: Patchstack │ │ Reported: 2026-04-29 by daroo │ │ Published: 2026-06-01 │ │ Fixed: Gravity Forms 2.10.1 │ └─────────────────────────────────────────────────────────────────────────┘ ``` | Reference | URL | Status | |---|---|---| | NVD | https://nvd.nist.gov/vuln/detail/CVE-2026-48866 | Verified | | Patchstack Advisory | https://patchstack.com/database/wordpress/plugin/gravityforms/vulnerability/wordpress-gravity-forms-plugin-2-10-0-1-arbitrary-file-deletion-vulnerability | Verified | | Gravity Forms Changelog | https://docs.gravityforms.com/gravityforms-change-log/ | Verified | | Source Code (Mirror) | https://github.com/codewurker/gravityforms | Verified | | Vulnerable Commit (v2.10.0) | https://github.com/codewurker/gravityforms/commit/86bf7b9ecf14fd4a826a741a1ae180040589ebed | Verified | | Patched Commit (v2.10.1) | https://github.com/codewurker/gravityforms/commit/cf2ff65133d581cfed1c308adc1621c3af1f8422 | Verified | | CWE-22 | https://cwe.mitre.org/data/definitions/22.html | Verified | | MITRE CVE | https://www.cve.org/CVERecord?id=CVE-2026-48866 | Not yet populated | --- ## VALIDITY **Valid.** Source code comparison between v2.10.0 ([`86bf7b9`](https://github.com/codewurker/gravityforms/commit/86bf7b9ecf14fd4a826a741a1ae180040589ebed)) and v2.10.1 ([`cf2ff65`](https://github.com/codewurker/gravityforms/commit/cf2ff65133d581cfed1c308adc1621c3af1f8422)) confirms the vulnerability: 1. `delete_physical_file()` in v2.10.0 calls `unlink()` without verifying the resolved path is within the uploads directory. 2. `get_physical_file_path()` uses `str_replace()` to convert URL base to filesystem path, preserving `../` sequences. 3. `get_multifile_value()` and `get_single_file_value()` validate URLs with `GFCommon::is_valid_url()`, which performs format-only validation. `../` passes. 4. The patch in v2.10.1 adds `GFCommon::is_file_in_uploads()` and `GFCommon::get_absolute_path()`, confirming the developers recognized and fixed this exact issue. The PoC logic follows the correct architecture. Implementation defects reduce its reliability. --- ## CVSS 3.1 Vector Breakdown Each finding below ties a specific PoC defect to the CVSS metric it undermines. ``` CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H AV:N Network — PoC correctly targets admin-ajax.php over HTTP AC:L Low complexity — PoC correctly requires only form ID + field ID PR:N No privileges — PoC correctly uses nopriv_gform_submit_form for injection UI:R User interaction — PoC correctly models admin deletion as the trigger S:C Changed scope — PoC does not verify scope change (deletion → RCE chain) C:H Confidentiality — PoC does not test data exfiltration paths I:H Integrity — PoC targets wp-config.php deletion (integrity destruction) A:H Availability — PoC verifies availability loss via HTTP health check ``` --- ## FINDINGS ### [F-01] Hardcoded form ID prevents exploitation of CVSS 9.6 Critical vuln for most targets **Severity:** Critical **CVSS Metric Affected:** AC:L (Attack Complexity: Low) **Location:** `poc.py:220` **Description:** The entry detail URL hardcodes `&id=1` regardless of the `--form-id` argument. WordPress sites with Gravity Forms forms having ID != 1 (the majority of real-world deployments, since form IDs increment across all forms) will fail without feedback. The nonce extraction returns nothing, and the code reports "Could not find delete nonce." This converts AC:L to AC:H: the attacker must know the form ID is exactly 1, or manually edit the code. **Evidence:** ```python # poc.py:220 entry_detail_url = f'{target}/wp-admin/admin.php?page=gf_entries&view=entry&id=1&lid={latest_entry_id}' # ^^^^ # hardcoded, ignores --form-id ``` Gravity Forms accepts `--form-id` at line 297 but never passes it to `trigger_deletion()`. The function signature at line 172 omits `form_id`: ```python def trigger_deletion(session, target, admin_user, admin_pass): # no form_id ``` **Fix:** ```python def trigger_deletion(session, target, admin_user, admin_pass, form_id): # ... entry_detail_url = f'{target}/wp-admin/admin.php?page=gf_entries&view=entry&id={form_id}&lid={latest_entry_id}' ``` --- ### [F-02] False positive on unclear response inflates AC:L assumption **Severity:** Critical **CVSS Metric Affected:** AC:L (Attack Complexity: Low) **Location:** `poc.py:157-161` **Description:** When the server returns HTTP 200 but the body contains none of the known success indicators (`gformRedirect`, `confirmation`, `thank`) or failure indicators (`validation_error`, `validation_message`), the function returns `True`. The exploit proceeds to Phase 2 assuming the poisoned entry exists. Many Gravity Forms configurations return generic HTML that matches none of these patterns: custom confirmation messages, external URL redirects, AJAX disabled, WAF challenge pages returning HTTP 200. CVSS assumes AC:L: the attacker submits the payload and it works. This defect converts the attack from deterministic to probabilistic. **Evidence:** ```python # poc.py:157-161 else: print(f'[?] Unclear response. Check manually.') print(f' Response snippet: {resp.text[:500]}') # May still have worked return True ``` **Fix:** ```python else: print(f'[-] Unclear response. Cannot confirm entry creation.') print(f' Response snippet: {resp.text[:500]}') return False ``` --- ### [F-03] Deletion targets latest entry, not the poisoned entry **Severity:** High **CVSS Metric Affected:** UI:R (User Interaction: Required), trigger phase reliability **Location:** `poc.py:216` **Description:** Phase 2 grabs the first entry ID from the entries list and deletes it. This may not be the poisoned entry. If the site has multiple forms, if entries were created between injection and deletion, or if the entries page orders by a different column, the wrong entry gets deleted. The poisoned entry persists in the database, and the target file survives. The CVSS vector models UI:R as "a privileged user must perform an action." The PoC models this as automated admin login + delete. If the automation deletes the wrong entry, the attack fails to satisfy UI:R for the poisoned entry. **Evidence:** ```python # poc.py:216 latest_entry_id = entry_ids[0] # assumes first entry is the poisoned one ``` No filtering by form ID, no verification that the entry contains the malicious URL. **Fix:** Parse the injection response for the created entry ID, or filter entries by form ID and verify the entry field value contains the traversal URL before deleting. --- ### [F-04] Verification only detects wp-config.php deletion **Severity:** High **CVSS Metric Affected:** A:H (Availability: High), ability to confirm impact **Location:** `poc.py:258-274` **Description:** `verify_file_exists()` sends an HTTP GET and checks for HTTP 500 or "error establishing a database connection." This only detects `wp-config.php` deletion. For `.htaccess`, plugin files, theme files, or any other target, the function returns `True` (site responding normally) regardless of whether the file was deleted. The operator receives false confirmation that the exploit failed. The CVSS vector rates A:H because deleting critical files renders the site non-functional. The PoC can only confirm this for one specific file. **Evidence:** ```python # poc.py:263-264 if resp.status_code == 200 and ('WordPress' in resp.text or '