CVE-2026-6248: wpForo Arbitrary File Deletion → RCE via Unsanitized Profile Field
wpForo ≤3.0.5 allows subscriber-level users to store arbitrary filesystem paths in file-type custom profile fields, which are later passed unvalidated to unlink(), enabling RCE via wp-config.php deletion.
A popular WordPress forum plugin has a serious security hole that could let someone with a regular user account delete files from your website — or worse, take over your entire server.
Here's what's happening. The wpForo plugin (used by thousands of WordPress sites to run discussion forums) has two security problems working together. First, when users update their profile information, the plugin doesn't properly check what data they're entering. Second, the plugin's safety filter for blocking sneaky file path tricks is too weak to actually stop someone determined to exploit it.
Think of it like a house with a lock on the door that checks whether you have a key, but then doesn't actually verify that the key works or where you're planning to go once you're inside.
An authenticated user — meaning someone with even basic forum access — could inject hidden commands into their profile that tell the server to delete files. A hacker could systematically destroy files needed to run your website, or plant malicious code that gives them complete control. This is why security experts call it a potential "remote code execution" vulnerability.
Who's at risk? Any website running wpForo Forum versions 3.0.5 or earlier. This includes community forums, support sites, and membership communities.
What should you do? First, update the plugin immediately when a patched version becomes available — don't wait. Second, if you're not actively using the forum plugin, consider disabling or removing it entirely. Third, limit who can create accounts on your forum, since this vulnerability requires user access to exploit. Check your plugin version now by logging into your WordPress dashboard and navigating to your plugins page.
Want the full technical analysis? Click "Technical" above.
CVE-2026-6248 is a two-part logic flaw in the wpForo Forum WordPress plugin (≤3.0.5) that chains an unsanitized write into a controlled delete. Any authenticated user with subscriber-level access can populate a file-type custom profile field with an arbitrary filesystem path. When that profile is saved again — or when the field's deletion routine fires — wpforo_fix_upload_dir() fails to remap the attacker-supplied path, and it lands directly in unlink(). Deleting wp-config.php forces WordPress into re-installation mode, which an attacker can immediately exploit for full RCE. CVSS 8.1 (HIGH), no authentication beyond subscriber required, no interaction from a victim needed.
Root cause:Members::update() stores attacker-controlled strings in file-type custom profile fields without validating that they represent a real upload path, and ucf_file_delete() passes those strings directly to unlink() when wpforo_fix_upload_dir() fails to match the expected upload-directory pattern.
Affected Component
Plugin: wpForo Forum — wpforo/wpforo.php
Versions: up to and including 3.0.5
Files of interest:
Two independent flaws combine. First, Members::update() iterates over all custom profile fields and persists field values to the database without checking whether a file-type field contains a real upload path.
// wpforo/includes/members/class-members.php
// Simplified pseudocode derived from plugin source structure
int Members::update(int user_id, array $data) {
// ... standard field validation for known fields ...
foreach (WPF()->cfield->get_cfields() as $cfield) {
$field_name = $cfield['slug'];
$field_type = $cfield['type']; // 'text', 'file', 'select', ...
if (isset($data[$field_name])) {
$value = $data[$field_name]; // attacker-supplied POST parameter
// BUG: no validation that $value is a real upload path for type='file'
// BUG: no wp_check_filetype(), no path_is_within() check against upload dir
if ($field_type === 'file') {
// Expected: $value = '/var/www/html/wp-content/uploads/wpforo/...'
// Actual: $value = '/var/www/html/wp-config.php' -- stored as-is
update_user_meta(user_id, 'wpforo_cf_' . $field_name, sanitize_text_field($value));
// sanitize_text_field() strips tags, trims whitespace -- does NOT validate path
}
}
}
return true;
}
Second, when a user updates their profile and the old file-type field value is replaced, ucf_file_delete() is called on the previously stored value to clean up the old file.
// wpforo/includes/functions/functions-ucf.php
string wpforo_fix_upload_dir(string $path) {
// Attempts to normalize a path inside the wpforo uploads directory.
// Returns the corrected absolute path if the path matches the expected
// wpforo upload subdirectory pattern; otherwise returns $path UNCHANGED.
$upload_dir = wp_upload_dir();
$upload_base = $upload_dir['basedir']; // e.g. /var/www/html/wp-content/uploads
$wpforo_dir = $upload_base . '/wpforo/';
// Pattern match: expects path to contain the wpforo upload subdir
if (strpos($path, $wpforo_dir) !== false) {
return str_replace($upload_base, $upload_dir['basedir'], $path); // normalize
}
// BUG: if $path does NOT contain $wpforo_dir (e.g. '/var/www/html/wp-config.php')
// the function returns $path completely unchanged, with no rejection or error
return $path;
}
void ucf_file_delete(string $file_path) {
// Called when a file-type custom field value is being replaced or cleared
$fixed_path = wpforo_fix_upload_dir($file_path);
// BUG: $fixed_path is passed directly to unlink() with no containment check
// If $file_path was attacker-controlled and outside the upload dir,
// wpforo_fix_upload_dir() returns it as-is, and we delete it unconditionally.
if (file_exists($fixed_path)) {
unlink($fixed_path); // <-- arbitrary file deletion
}
}
The critical observation is that wpforo_fix_upload_dir() is a remapping helper, not a guard. It was never intended to reject out-of-scope paths — only to normalize valid ones. The absence of a hard containment assertion after the call is the exploitable gap.
Exploitation Mechanics
EXPLOIT CHAIN:
1. Register or authenticate as any subscriber-level WordPress user.
2. Identify a file-type custom profile field on the wpForo profile form.
(Admin-created custom fields of type 'file' are the attack surface.)
3. Submit a profile update POST request with the file field set to a target path:
POST /wp-admin/admin-ajax.php
action=wpforo_update_profile
&wpf_cf_avatar_file=/var/www/html/wp-config.php ← arbitrary path
4. Members::update() calls sanitize_text_field() on the value (harmless),
then stores it verbatim in user_meta:
wp_usermeta: meta_key='wpforo_cf_avatar_file'
meta_value='/var/www/html/wp-config.php'
5. Submit a second profile update on the same field with any new value
(or an empty value to trigger the delete-old-file path).
6. wpForo detects the field value has changed; calls:
ucf_file_delete('/var/www/html/wp-config.php')
-> wpforo_fix_upload_dir('/var/www/html/wp-config.php')
// '/var/www/html/wp-config.php' does NOT contain '/uploads/wpforo/'
// function returns path unchanged
-> file_exists('/var/www/html/wp-config.php') === true
-> unlink('/var/www/html/wp-config.php') ← wp-config.php deleted
7. WordPress detects missing wp-config.php, enters setup wizard mode.
8. Attacker navigates to /wp-admin/setup-config.php, supplies attacker-
controlled database credentials, and completes installation.
Full administrative RCE achieved.
ALTERNATE HIGH-VALUE TARGETS:
- /var/www/html/.htaccess → disable mod_security, enable handlers
- /var/www/html/wp-includes/class-wp.php → DoS / partial RCE surface
- /etc/cron.d/ → if www-data has read-accessible path
- Any plugin file disabling security checks when absent
Memory Layout
This is a logic/filesystem vulnerability rather than a memory corruption bug; there is no heap smash. The relevant "layout" is the data flow through WordPress's object cache and the usermeta table.
The correct fix requires changes in both locations. wpforo_fix_upload_dir() must become a hard containment guard, and Members::update() must validate that stored file-field values were placed there by the plugin's own upload mechanism.
// BEFORE (vulnerable) — wpforo_fix_upload_dir():
string wpforo_fix_upload_dir(string $path) {
$upload_dir = wp_upload_dir();
$wpforo_dir = $upload_dir['basedir'] . '/wpforo/';
if (strpos($path, $wpforo_dir) !== false) {
return str_replace(...); // normalize
}
return $path; // BUG: returns arbitrary path unchanged
}
// AFTER (patched):
string wpforo_fix_upload_dir(string $path) {
$upload_dir = wp_upload_dir();
$wpforo_dir = realpath($upload_dir['basedir'] . '/wpforo/');
$real_path = realpath($path);
// Hard containment: reject anything outside the wpforo upload directory
if ($real_path === false || strpos($real_path, $wpforo_dir) !== 0) {
return ''; // empty string → caller must treat as invalid
}
return $real_path;
}
// BEFORE (vulnerable) — ucf_file_delete():
void ucf_file_delete(string $file_path) {
$fixed_path = wpforo_fix_upload_dir($file_path);
if (file_exists($fixed_path)) {
unlink($fixed_path); // BUG: no containment assertion
}
}
// AFTER (patched):
void ucf_file_delete(string $file_path) {
$fixed_path = wpforo_fix_upload_dir($file_path);
if (empty($fixed_path)) {
// Path failed containment check; log and abort
error_log('wpforo: ucf_file_delete rejected out-of-scope path: ' . $file_path);
return;
}
if (file_exists($fixed_path)) {
unlink($fixed_path);
}
}
// BEFORE (vulnerable) — Members::update() file field handling:
if ($field_type === 'file') {
update_user_meta($user_id, 'wpforo_cf_' . $field_name,
sanitize_text_field($value)); // BUG: no path validation
}
// AFTER (patched):
if ($field_type === 'file') {
// Validate that the value was produced by the plugin's own upload handler.
// Only accept paths already confirmed to be inside the wpforo upload dir.
$upload_dir = wp_upload_dir();
$wpforo_dir = realpath($upload_dir['basedir'] . '/wpforo/');
$real_value = realpath($value);
if ($real_value === false || strpos($real_value, $wpforo_dir) !== 0) {
// Reject: value is not a plugin-managed upload path
continue;
}
update_user_meta($user_id, 'wpforo_cf_' . $field_name, $real_value);
}
Detection and Indicators
Web server / PHP logs — look for profile update requests where a file-type custom field POST parameter contains an absolute path outside the WordPress upload directory:
# Apache / Nginx access log pattern (suspicious profile update)
POST /wp-admin/admin-ajax.php ... "action=wpforo_update_profile" 200
# PHP error log (if patched version installed after exploitation attempt)
[warn] wpforo: ucf_file_delete rejected out-of-scope path: /var/www/html/wp-config.php
# Filesystem audit (auditd rule)
-a always,exit -F arch=b64 -S unlink,unlinkat \
-F path=/var/www/html/wp-config.php -k wpforo_rce
# MySQL query log — look for user_meta writes with suspicious meta_value
SELECT * FROM wp_usermeta
WHERE meta_key LIKE 'wpforo_cf_%'
AND meta_value NOT LIKE '%/wp-content/uploads/wpforo/%';
WordPress-side indicator: the presence of rows in wp_usermeta where a wpforo_cf_ meta key holds an absolute filesystem path outside the expected upload subdirectory is a near-certain indicator of exploitation or probing.
Remediation
Update immediately to the patched version of wpForo Forum once released by the vendor (confirm version > 3.0.5 in your plugin list).
Audit existing user_meta for malicious paths using the SQL query above before upgrading — an attacker may have pre-staged a path that fires on next profile edit.
Harden filesystem permissions: ensure wp-config.php is owned by root or a non-webserver user (chown root:root wp-config.php; chmod 400 wp-config.php). This does not eliminate the vulnerability but prevents unlink() from succeeding under the www-data process.
WAF rule: block POST bodies to admin-ajax.php where any parameter value matches ^/.*\.(php|conf|htaccess|ini)$.
Disable file-type custom profile fields in wpForo admin settings as a temporary mitigation until the patch is applied.