## Summary
Divi Form Builder for WordPress exposes its file-upload handler through WordPress AJAX action `de_fb_image_upload`, which is registered to the PHP method `do_image_upload()` for unauthenticated callers. In the original Divi Form Builder v5.1.2 build used in this run, the handler reads the attacker-controlled POST parameter `acceptFileTypes` and interpolates it into the server-side upload validation regex. An unauthenticated attacker who obtains a public form nonce can set `acceptFileTypes=phtml|phar|php5|php7`, upload a PHP-executable file such as `.phtml` into `/wp-content/uploads/de_fb_uploads/`, and then request that file over HTTP to execute PHP code under the web-server user.

## Impact
- **Package/component affected:** Divi Form Builder WordPress plugin (`wordpress:divi-form-builder`), specifically `includes/classes/init.class.php::do_image_upload()` and the upload storage handled by `includes/de_fb_file_upload.php`.
- **Affected versions:** The ticket claims all versions up to and including 5.1.8. This run reproduced the issue against an original Divi Form Builder v5.1.2 plugin archive (`sha256=85bf508b12f6d6152f06f8c5eb3143963c3446a7338e90e1de180f5da8a51c34`).
- **Risk level and consequences:** Critical. The vulnerable path is reachable by unauthenticated HTTP requests with a valid public form nonce. Successful exploitation allows arbitrary PHP payload upload and web request execution as the Apache/PHP user (`www-data` in this reproduction), which is remote code execution.

## Impact Parity
- **Disclosed/claimed maximum impact:** Remote code execution via unauthenticated arbitrary file upload.
- **Reproduced impact from this run:** Full PHP code execution through the live WordPress/Apache product path. The uploaded `.phtml` payload executed and returned `uid=33(www-data) gid=33(www-data) groups=33(www-data)` from `system('id')`.
- **Parity:** `full`.
- **Not demonstrated:** No post-exploitation persistence or privilege escalation beyond the web-server user was attempted; this is not needed to prove the claimed RCE impact.

## Root Cause
The original plugin registers the public AJAX upload action to `do_image_upload()`:

- `includes/classes/init.class.php` registers `wp_ajax_de_fb_image_upload` and `wp_ajax_nopriv_de_fb_image_upload` to `do_image_upload()`.
- `do_image_upload()` reads `$_POST['acceptFileTypes']` and constructs upload validation options as `'/\.(' . $accept_file_types . ')$/i'`.
- `includes/de_fb_file_upload.php` validates the uploaded filename with `preg_match($this->options['accept_file_types'], $file->name)`, then writes the file to `wp-content/uploads/de_fb_uploads/` and returns a public URL.

Because the extension allow-list comes from the request, the attacker controls the server-side validation pattern. Supplying `phtml|phar|php5|php7` makes `.phtml` pass validation. In the tested Apache/PHP environment, `.phtml` is handled by PHP, so requesting `/wp-content/uploads/de_fb_uploads/payload.phtml` executes the attacker-controlled payload. The plugin’s upload-directory protection is insufficient for this class of extension bypass and is also server dependent.

No upstream fixed commit was provided in the ticket. For the negative control, `reproduction_steps.sh` applies a line-accurate local hardening patch over the same original plugin tree so that `do_image_upload()` rejects executable extension values before constructing the upload handler options. The patched control returns `{"success":false,"data":{"message":"Executable file types are not allowed."}}` and the corresponding uploaded URL is absent/404.

## Reproduction Steps
1. Run `bundle/repro/reproduction_steps.sh` from the bundle root (or set `PRUVA_ROOT` to the bundle path).
2. The script installs/starts a real WordPress stack with Apache, mod_php, and MariaDB, installs the original Divi Form Builder v5.1.2 plugin archive, creates a public page containing a form nonce, and sends unauthenticated multipart POST requests to `/wp-admin/admin-ajax.php` with `action=de_fb_image_upload`, `fb_nonce`, attacker-controlled `acceptFileTypes`, and a `.phtml` payload.
3. It performs two vulnerable attempts and then swaps in a patched copy of the same original plugin tree for two fixed negative-control attempts.
4. Expected evidence: vulnerable attempts return an upload JSON object containing `/wp-content/uploads/de_fb_uploads/payload_vuln_*.phtml`; subsequent GET requests to those URLs execute PHP and return the marker plus `id` output. Fixed attempts return the JSON rejection message and the payload URL returns 404/no marker.

## Evidence
- Main run log: `bundle/logs/reproduction_steps.log`
- Runtime manifest: `bundle/repro/runtime_manifest.json`
- Original source identity: `bundle/logs/source_identity.txt`
- Vulnerable attempt artifacts:
  - `bundle/logs/vuln_attempt_1/upload_response.json`
  - `bundle/logs/vuln_attempt_1/execution_response.txt`
  - `bundle/logs/vuln_attempt_1/result.json`
  - `bundle/logs/vuln_attempt_2/upload_response.json`
  - `bundle/logs/vuln_attempt_2/execution_response.txt`
  - `bundle/logs/vuln_attempt_2/result.json`
- Fixed negative-control artifacts:
  - `bundle/logs/fixed_attempt_1/upload_response.json`
  - `bundle/logs/fixed_attempt_1/execution_response.txt`
  - `bundle/logs/fixed_attempt_1/result.json`
  - `bundle/logs/fixed_attempt_2/upload_response.json`
  - `bundle/logs/fixed_attempt_2/execution_response.txt`
  - `bundle/logs/fixed_attempt_2/result.json`
- Patch used for negative control: `bundle/logs/fixed_patch.diff`
- HTTP server evidence: `bundle/logs/apache_access.log`, `bundle/logs/apache_error.log`

Key vulnerable evidence excerpt from `bundle/logs/vuln_attempt_1/result.json`:

```json
{
  "upload_http_code": "200",
  "execution_http_code": "200",
  "uploaded_or_accepted": true,
  "rce_observed": true,
  "execution_response_excerpt": "PRUVA_DFB_RCE_MARKER=PRUVA_CVE_2026_5524_vuln_1\nPHP_EXECUTED_AS=www-data\nCMD_OUTPUT_BEGIN\nuid=33(www-data) gid=33(www-data) groups=33(www-data)\n\nCMD_OUTPUT_END\n"
}
```

Key fixed-control evidence excerpt from `bundle/logs/fixed_attempt_1/result.json`:

```json
{
  "upload_http_code": "200",
  "execution_http_code": "404",
  "uploaded_or_accepted": false,
  "rce_observed": false,
  "upload_response_excerpt": "{\"success\":false,\"data\":{\"message\":\"Executable file types are not allowed.\"}}",
  "server_marker": "(no marker)"
}
```

The runtime manifest confirms the API surface was exercised:

```json
{
  "entrypoint_kind": "api_remote",
  "service_started": true,
  "healthcheck_passed": true,
  "target_path_reached": true
}
```

## Recommendations / Next Steps
- Do not accept a file-extension allow-list from the client. Treat UI-supplied accepted file types as advisory only; server-side validation must use a trusted form configuration stored server-side.
- Explicitly reject all executable PHP extensions and other server-executable types regardless of the client-supplied field configuration.
- Store uploads in a non-executable location, or serve them through a controlled download endpoint rather than directly from the web root.
- Harden web-server configuration for upload directories on Apache and Nginx; do not rely only on `.htaccess` rules because Nginx ignores them and extension mappings differ by server.
- Add regression tests that POST to `admin-ajax.php?action=de_fb_image_upload` with executable extensions (`.php`, `.phtml`, `.phar`, `.php5`, `.php7`) and assert that all are rejected and no public executable file is written.
- Upgrade to a vendor-fixed release once available and verify that the fix covers both validation and upload-directory execution controls.

## Additional Notes
- The script was run successfully twice consecutively after correcting setup and Apache cache behavior.
- The previous rejected surrogate-plugin approach was replaced with an original Divi Form Builder v5.1.2 plugin archive and the real WordPress AJAX endpoint.
- The AJAX action name is `de_fb_image_upload`; the original product maps that action to the requested `do_image_upload()` method through WordPress `wp_ajax_*` hooks.
- The negative control is a local patch over the same original project path because the ticket did not provide a fixed commit hash or official patched archive.
