The plugin uses a general set of defaults for file fields. When file upload fields are used for non-private information, this is adequate. This is the case for using them for custom avatars, or shared public documents. This is not well suited to use for something more private and even though the plugin sets up index.php and .htaccess files to prevent directory browsing, user IDs and file names can be guessed and malicious actors can probe for these values.
To prevent a malicious actor from discovering potential user uploaded files, you can set up a few filters to obfuscate the directory and file names used. The examples in this FAQ will cover changing three components of the path from the defaults:
- The WP-Members file directory in the WP uploads (default value: “wpmembers”)
- The user upload directory within the WP-Members directory above (default value: user ID)
- The actual file name (obfuscating this to random values to prevent probing for names like “passport.pdf”, “drivers-license.pdf”, “application.pdf”, etc.
Putting together these three elements will change a file path from this:
wpmembers/user_files/134/my_file.pdf
to something like this:
userfiles_p28MEZeXIo1mC8OI3HI1QU0JqGTdIZgK/15NaHvFFoRbpD63O6DFCfysoI7dVY0yd6t5B/b691a2452ea1b529dcc2ad7e12d7474ed644.pdf
tldr; Each part of this is explained in detail below, but each of the three code snippets is copy/paste ready and will give you the above result.
Changing the WP-Members upload directory
This makes use of the main WP-Members settings filter hook wpmem_settings to change the name of the main WP-Members upload directory. The following code snippet will use a random hash value as part of the directory name. You can make this long or short – the example makes the hash 32 characters long.
/**
* Use a random hash as the plugin's general upload directory name.
*
* This example uses a random string length of 32 chars, but you
* can change the value to whatever suits (longer or shorter).
*/
add_filter( 'wpmem_settings', function( $settings ) {
// How long of a hash?
$hash_len = 32;
// Check if we already created a hash.
$hash = get_option( 'wpmem_file_dir_hash' );
if ( ! $hash ) {
// If there is no existing hash, create with wp_generate_password().
$hash = wp_generate_password( $hash_len, false, false );
update_option( 'wpmem_file_dir_hash', $hash );
}
/*
* What format of the main directory is desired?
* This example makes it "userfiles_2NUMkyuWC08n09".
* The directory does not need to include "wpmembers" in the name.
* It can be whatever name is desired. Or, it can be just the
* hash value as the name. It's up to the site admin.
*/
$settings['upload_base'] = "userfiles_" . $hash;
return $settings;
});Changing the WP-Members user directory names
When WP-Members uploads a user file, it uses the user ID to create the folder name. We can adjust this to a more random value similar to how we hashed the primary upload directory using the wpmem_user_upload_dir filter hook. However, in this case, we’re using a random hash for each user. The following code snippet creates a 32 character value, but it uses the user ID as the first part of the string, thus avoiding the need to test each hash for uniqueness. Like the first code snippet above, you can adjust the hash length as desired.
/**
* Add a random hash to the user upload directory.
*
* Change the default name of the user upload directory.
* This example completely removes the user ID value from the
* directory name, leaving it as a completely random string.
*
* Change $hash_len to set the length. Example creates a random
* string 36 characters long.
*/
add_filter( 'wpmem_user_upload_dir', function( $args ) {
// How long of a hash?
$hash_len = 36;
// Check if user already has a directory hash.
$hash = get_user_meta( $args['user_id'], 'wpmem_file_dir_hash', true );
if ( ! $hash ) {
/*
* If there is no existing hash, we need to create one.
*
* To make sure it is unique without having to do a for/while loop
* while checking the db, we can use the user ID in the string.
* Since the random hash is already long, inserting the user ID
* does not degrade the randomness. Theoretically, one could just
* add the user ID without this step, but I like to have everything
* being the same length so that a one digit user ID results in the
* same directory name length as a four digit user ID.
*
* This example makes it user ID + hash. For example, where the
* user ID is "234", the resulting direcotry would be like this:
*
* 234LXd2B8SE31u19TAS0SnwQW6ji
*
* For ultimate randomness in this example, consider switching to
* add the user ID to the end rather than the beginning. I did it
* at the beginning to allow a user with file system access to be
* able to browse the directory by user ID if they understood the
* directory name construction being done here.
*/
$uid_len = strlen( $args['user_id'] );
$hash = $args['user_id'] . wp_generate_password( ($hash_len-$uid_len), false, false );
update_user_meta( $args['user_id'], 'wpmem_file_dir_hash', $hash );
}
$args['user_dir'] = $hash;
return $args;
});Changing the file name to something random
The last part of this is to randomize file names. We can do this using the core WP filter sanitize_file_name.
/**
* Completely randomize the filename of the upload.
*
* Change the variable $hash_len based on the random
* string length desired. Example creates a random
* string 36 characters long.
*/
add_filter( 'sanitize_file_name', function( $filename ) {
// How long a random string do we want?
$hash_len = 36;
// Get the file extension.
$ext = pathinfo( $filename, PATHINFO_EXTENSION );
if ( preg_match( '/^[a-f0-9]{'. $hash_len . '}-.*/', $filename ) ) {
$filename = substr( $filename, $hash_len + 1 );
}
$key = sha1( random_bytes(32) );
$key = substr( $key, 0, $hash_len );
return "$key.$ext";
}, 10 );