How We Audited a High-Traffic WooCommerce Enterprise Stack on Google Cloud and Mitigated Remote Code Execution (RCE) via insecure file uploads
Initial Reconnaissance and Attack Vector Identification
Our engagement began with a deep dive into the existing infrastructure and application stack. The client, a large enterprise operating a high-traffic WooCommerce store on Google Cloud Platform (GCP), had reported suspicious activity and a potential security breach. The primary concern was a suspected Remote Code Execution (RCE) vulnerability, likely stemming from insecure file upload functionalities within custom plugins or themes.
The stack comprised:
- Google Kubernetes Engine (GKE) for application hosting.
- Cloud SQL (PostgreSQL) for the database.
- Cloud Storage for media assets.
- Cloud Load Balancing for traffic management.
- A complex web of custom PHP modules and a heavily modified WooCommerce installation.
The initial reconnaissance phase focused on identifying potential entry points. We leveraged a combination of automated scanning tools and manual inspection of the application’s source code. Key areas of interest included:
- User-facing forms, especially those allowing file uploads (e.g., profile pictures, product images, support ticket attachments).
- API endpoints, particularly those handling data serialization or deserialization.
- Third-party plugin integrations, as these are often less scrutinized than core application code.
- Admin interfaces and their associated functionalities.
Analyzing the Insecure File Upload Vulnerability
The critical vulnerability was identified in a custom-built plugin responsible for allowing vendors to upload product images. The plugin’s file upload handler lacked robust validation, allowing arbitrary file types to be uploaded and, crucially, did not properly sanitize filenames or restrict execution permissions on uploaded files. This created a direct pathway for RCE.
Let’s examine a simplified, illustrative snippet of the vulnerable PHP code. Note that this is a conceptual representation; the actual code was more convoluted but exhibited the same fundamental flaws.
Vulnerable Code Snippet (Conceptual)
<?php
// Simplified example of a vulnerable file upload handler
if (isset($_FILES['product_image']) && $_FILES['product_image']['error'] === UPLOAD_ERR_OK) {
$file_tmp_path = $_FILES['product_image']['tmp_name'];
$file_name = basename($_FILES['product_image']['name']); // No sanitization
$upload_dir = '/var/www/html/uploads/product_images/'; // Hardcoded, potentially writable
// No file type validation (e.g., checking MIME type or extension)
// No check for executable content within the file
// Move the file without proper security checks
if (move_uploaded_file($file_tmp_path, $upload_dir . $file_name)) {
// File uploaded successfully, but could be an executable script
echo "File uploaded successfully.";
} else {
echo "Error uploading file.";
}
}
?>
The core issues here are:
- Lack of File Type Validation: The code does not check the MIME type or the file extension. An attacker could upload a file named
shell.php.jpgor even a file with a valid image extension but containing PHP code. - No Filename Sanitization:
basename()is insufficient. It removes directory traversal sequences but doesn’t prevent overwriting existing files or uploading files with malicious names (e.g., containing null bytes or control characters). - Unrestricted Upload Directory: The
$upload_diris directly concatenated with the filename. If this directory is web-accessible and has write permissions for the web server user, an attacker can upload executable scripts. - No Content Inspection: The code doesn’t inspect the *content* of the uploaded file for malicious payloads.
Exploitation Scenario: Uploading a Web Shell
An attacker could craft a malicious PHP file (e.g., a simple web shell) and upload it using the vulnerable plugin. The attacker would typically rename the file to bypass any rudimentary extension checks (e.g., shell.php.jpg) and then access it directly via its URL on the web server.
Consider a basic PHP web shell:
<?php
// A simple web shell
if (isset($_REQUEST['cmd'])) {
echo '<pre>';
system($_REQUEST['cmd']);
echo '</pre>';
die;
}
?>
If this file, named backdoor.php, were uploaded to /var/www/html/uploads/product_images/, an attacker could then execute arbitrary commands by navigating to:
http://your-woocommerce-domain.com/uploads/product_images/backdoor.php?cmd=ls -la
This would execute the ls -la command on the server, revealing directory contents and confirming RCE. From here, an attacker could escalate privileges, exfiltrate data, or deploy ransomware.
Mitigation Strategy: Secure File Uploads
The mitigation involved a multi-layered approach, addressing both the immediate vulnerability and implementing best practices for future development.
1. Server-Side Validation and Sanitization
The most critical step was to rewrite the file upload handler to incorporate robust validation. This includes:
- Strict File Type Whitelisting: Only allow explicitly permitted MIME types and file extensions (e.g.,
image/jpeg,.jpg,.png). - Filename Sanitization: Generate unique, random filenames for uploaded files to prevent collisions and malicious naming.
- Content Inspection: For image uploads, consider using libraries to re-process the image, stripping any embedded executable code.
- Restricted Upload Directory Permissions: Ensure the upload directory is not directly web-accessible and that the web server user has minimal write permissions.
- Move Uploads Outside Web Root: Ideally, uploaded files should be stored outside the web server’s document root and served via a secure, controlled mechanism (e.g., a dedicated download endpoint that checks permissions).
Here’s an example of a more secure PHP upload handler:
<?php
// Secure file upload handler example
// Configuration
define('UPLOAD_DIR', '/var/www/secure_uploads/product_images/'); // Outside web root
define('ALLOWED_MIME_TYPES', ['image/jpeg', 'image/png', 'image/gif']);
define('ALLOWED_EXTENSIONS', ['jpg', 'jpeg', 'png', 'gif']);
define('MAX_FILE_SIZE', 5 * 1024 * 1024); // 5MB
function sanitize_filename($filename) {
// Remove non-alphanumeric characters, replace spaces with underscores
$filename = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $filename);
// Prevent directory traversal (though basename should handle this)
$filename = basename($filename);
return $filename;
}
function generate_unique_filename($extension) {
return uniqid('img_', true) . '.' . $extension;
}
if (isset($_FILES['product_image']) && $_FILES['product_image']['error'] === UPLOAD_ERR_OK) {
$file_tmp_path = $_FILES['product_image']['tmp_name'];
$original_filename = $_FILES['product_image']['name'];
$file_size = $_FILES['product_image']['size'];
$file_mime_type = mime_content_type($file_tmp_path); // Requires fileinfo extension
// 1. Check file size
if ($file_size > MAX_FILE_SIZE) {
die("Error: File is too large.");
}
// 2. Check allowed MIME types
if (!in_array($file_mime_type, ALLOWED_MIME_TYPES)) {
die("Error: Invalid file type (MIME).");
}
// 3. Get file extension and check against allowed list
$file_extension = strtolower(pathinfo($original_filename, PATHINFO_EXTENSION));
if (!in_array($file_extension, ALLOWED_EXTENSIONS)) {
die("Error: Invalid file type (extension).");
}
// 4. Sanitize and generate a unique filename
$sanitized_original_name = sanitize_filename($original_filename);
$new_filename = generate_unique_filename($file_extension);
$destination_path = UPLOAD_DIR . $new_filename;
// Ensure upload directory exists and is writable by the web server
if (!is_dir(UPLOAD_DIR)) {
if (!mkdir(UPLOAD_DIR, 0755, true)) {
die("Error: Could not create upload directory.");
}
}
// 5. Move the file
if (move_uploaded_file($file_tmp_path, $destination_path)) {
// Optional: Image re-processing to strip metadata/potential exploits
// Example: Using GD or Imagick to re-save the image
echo "File uploaded successfully as: " . $new_filename;
// Store $new_filename in the database associated with the product
} else {
die("Error: Failed to move uploaded file.");
}
} else {
// Handle file upload errors (e.g., UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE)
switch ($_FILES['product_image']['error']) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
die("Error: File size exceeds limit.");
case UPLOAD_ERR_PARTIAL:
die("Error: File upload was only partially completed.");
case UPLOAD_ERR_NO_FILE:
die("Error: No file was uploaded.");
case UPLOAD_ERR_NO_TMP_DIR:
die("Error: Missing temporary folder.");
case UPLOAD_ERR_CANT_WRITE:
die("Error: Failed to write to disk.");
case UPLOAD_ERR_EXTENSION:
die("Error: A PHP extension stopped the file upload.");
default:
die("Error: Unknown upload error.");
}
}
?>
2. Infrastructure-Level Controls (GCP)
Beyond application code, infrastructure plays a vital role:
- GKE Network Policies: Restrict ingress and egress traffic between pods. Ensure that pods handling file uploads cannot directly communicate with sensitive internal services or external command execution endpoints.
- Cloud Storage Security: If using Cloud Storage for uploads, configure appropriate IAM roles and bucket policies. Avoid making buckets publicly writable. Implement signed URLs for uploads and downloads if direct access is required.
- Web Application Firewall (WAF): Deploy a WAF (e.g., Cloud Armor on GCP) to filter malicious requests, including those attempting to exploit known file upload vulnerabilities or inject commands. Configure rules to detect suspicious patterns in file uploads.
- Container Security Scanning: Regularly scan container images for vulnerabilities.
- Least Privilege Principle: Ensure the Kubernetes service account and the container’s runtime user have only the necessary permissions. The web server process should not have write access to directories containing executable code.
3. Code Review and Security Testing
Implementing a robust security development lifecycle (SDL) is paramount:
- Mandatory Code Reviews: All code changes, especially those involving file handling, authentication, or external input, must undergo peer review with a security focus.
- Static Application Security Testing (SAST): Integrate SAST tools into the CI/CD pipeline to automatically detect common vulnerabilities like insecure file uploads.
- Dynamic Application Security Testing (DAST): Regularly perform DAST scans against staging and production environments to identify runtime vulnerabilities.
- Penetration Testing: Conduct periodic penetration tests by independent security professionals.
Post-Mitigation Verification and Monitoring
After implementing the secure file upload handler and infrastructure controls, thorough verification was essential. This involved:
- Re-testing the Vulnerability: Attempting to upload various malicious file types (e.g.,
.php,.phtml, double-extension files, files with null bytes) to ensure they are rejected. - Testing Edge Cases: Verifying behavior with oversized files, zero-byte files, and files with unusual characters in their names.
- Log Analysis: Reviewing web server logs, application logs, and GCP audit logs for any suspicious activity related to file uploads or attempted command execution.
- Implementing Intrusion Detection Systems (IDS): Deploying or enhancing IDS to monitor for known attack patterns.
- Setting Up Alerting: Configuring alerts for critical security events, such as repeated failed upload attempts, WAF rule triggers, or unusual process execution within containers.
The incident highlighted the critical importance of treating file uploads as a high-risk operation. By combining secure coding practices, rigorous validation, and robust infrastructure controls, enterprises can significantly reduce their attack surface and protect against devastating RCE vulnerabilities.