--- a/g-ffl-checkout/admin/class-ffl-api-admin.php
+++ b/g-ffl-checkout/admin/class-ffl-api-admin.php
@@ -3369,13 +3369,11 @@
$document_type = sanitize_text_field($_POST['document_type']);
$order_id = intval($_POST['order_id']);
$file = $_FILES['document'];
-
- // Validate file type
- $allowed_types = array('pdf', 'jpg', 'jpeg', 'png', 'gif');
- $file_extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
-
- if (!in_array($file_extension, $allowed_types)) {
- wp_send_json_error(array('message' => 'Invalid file type. Only PDF, JPG, JPEG, PNG, and GIF files are allowed.'));
+
+ // Validate file type (server-side sniffing)
+ $validation = $this->validate_uploaded_file($file);
+ if (!$validation['valid']) {
+ wp_send_json_error(array('message' => $validation['message']));
}
// Check file size (5MB limit)
@@ -3392,8 +3390,15 @@
wp_send_json_error(array('message' => 'Failed to create upload directory'));
}
}
-
- // Generate unique filename with admin prefix for consistency
+
+ // Best-effort protection files (portable across servers)
+ $this->maybe_add_directory_protection_files($ffl_documents_dir);
+
+ // Generate unique filename with admin prefix for consistency (use validated extension)
+ $file_extension = isset($validation['ext']) ? strtolower($validation['ext']) : '';
+ if (empty($file_extension)) {
+ wp_send_json_error(array('message' => 'Invalid file type'));
+ }
$unique_filename = 'admin_' . $document_type . '_' . $order_id . '_' . uniqid() . '_' . time() . '.' . $file_extension;
$file_path = $ffl_documents_dir . $unique_filename;
@@ -3892,7 +3897,7 @@
<option value="drivers_license">Driver's License</option>
<option value="other">Other</option>
</select>
- <input type="file" id="admin_document_file" accept=".pdf,.jpg,.jpeg,.png,.gif" style="width: 48%;">
+ <input type="file" id="admin_document_file" accept="application/pdf,image/*" style="width: 48%;">
</div>
<button type="button" id="admin_upload_document" class="button button-primary button-small"
data-order="<?php echo esc_attr($order_id); ?>" style="margin-top: 6px;">Upload Document</button>
@@ -4161,7 +4166,7 @@
}
// Upload file
- $upload_result = $this->upload_admin_document($file, $document_type, $order_id);
+ $upload_result = $this->upload_admin_document($file, $document_type, $order_id, $validation);
if (!$upload_result['success']) {
wp_send_json_error(array('message' => $upload_result['message']));
}
@@ -4272,36 +4277,101 @@
if ($file['size'] > $max_size) {
return array('valid' => false, 'message' => 'File size must be less than 10MB');
}
-
- // Check file type
- $allowed_types = array('application/pdf', 'image/jpeg', 'image/jpg', 'image/png', 'image/gif');
- $file_type = $file['type'];
-
- if (!in_array($file_type, $allowed_types)) {
- return array('valid' => false, 'message' => 'Only PDF, JPEG, PNG, and GIF files are allowed');
+
+ if (empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
+ return array('valid' => false, 'message' => 'Invalid upload');
+ }
+
+ // Verify file type using WordPress sniffing (do not trust browser-provided MIME)
+ $allowed_mimes = array(
+ 'pdf' => 'application/pdf',
+ 'jpg' => 'image/jpeg',
+ 'jpeg' => 'image/jpeg',
+ 'png' => 'image/png',
+ 'gif' => 'image/gif',
+ 'webp' => 'image/webp',
+ );
+
+ $filetype = wp_check_filetype_and_ext($file['tmp_name'], $file['name'], $allowed_mimes);
+
+ if (empty($filetype) || empty($filetype['ext']) || empty($filetype['type'])) {
+ return array('valid' => false, 'message' => 'Only PDF and image files (JPG, PNG, GIF, WebP) are allowed');
+ }
+
+ return array('valid' => true, 'ext' => $filetype['ext'], 'type' => $filetype['type']);
+ }
+
+ /**
+ * Best-effort protection files for upload directories.
+ *
+ * This plugin serves documents via authenticated download handlers; these files
+ * are defense-in-depth and must not break sites on servers that ignore or
+ * restrict per-directory config (e.g., Nginx, IIS, or Apache with limited AllowOverride).
+ */
+ private function maybe_add_directory_protection_files($dir_path) {
+ if (empty($dir_path) || !is_dir($dir_path) || !is_writable($dir_path)) {
+ return;
+ }
+
+ $dir_path = rtrim($dir_path, '/');
+
+ // Prevent directory listing on many servers
+ $index_path = $dir_path . '/index.php';
+ if (!file_exists($index_path)) {
+ @file_put_contents($index_path, "<?phpn// Silence is golden.n");
+ }
+
+ $server_software = isset($_SERVER['SERVER_SOFTWARE']) ? strtolower((string) $_SERVER['SERVER_SOFTWARE']) : '';
+ $looks_like_apache = (strpos($server_software, 'apache') !== false) || (strpos($server_software, 'litespeed') !== false);
+ $looks_like_iis = (strpos($server_software, 'microsoft-iis') !== false) || (strpos($server_software, 'iis') !== false);
+
+ // Apache/LiteSpeed: add an .htaccess deny file when likely supported
+ $htaccess_path = $dir_path . '/.htaccess';
+ if ($looks_like_apache && !file_exists($htaccess_path)) {
+ $htaccess_content = "# Prevent direct web access to uploaded documents.n";
+ $htaccess_content .= "<IfModule mod_authz_core.c>n";
+ $htaccess_content .= "Require all deniedn";
+ $htaccess_content .= "</IfModule>n";
+ $htaccess_content .= "<IfModule !mod_authz_core.c>n";
+ $htaccess_content .= "Deny from alln";
+ $htaccess_content .= "</IfModule>n";
+ @file_put_contents($htaccess_path, $htaccess_content);
+ }
+
+ // IIS: add web.config deny file when applicable
+ $web_config_path = $dir_path . '/web.config';
+ if ($looks_like_iis && !file_exists($web_config_path)) {
+ $web_config_content = "<?xml version="1.0" encoding="UTF-8"?>n";
+ $web_config_content .= "<configuration>n";
+ $web_config_content .= " <system.webServer>n";
+ $web_config_content .= " <authorization>n";
+ $web_config_content .= " <deny users="*" />n";
+ $web_config_content .= " </authorization>n";
+ $web_config_content .= " </system.webServer>n";
+ $web_config_content .= "</configuration>n";
+ @file_put_contents($web_config_path, $web_config_content);
}
-
- return array('valid' => true);
}
/**
* Upload document to secure directory for admin
*/
- private function upload_admin_document($file, $document_type, $order_id) {
+ private function upload_admin_document($file, $document_type, $order_id, $validation) {
// Create secure upload directory if it doesn't exist
$upload_dir = wp_upload_dir();
$ffl_upload_dir = $upload_dir['basedir'] . '/ffl-documents';
if (!file_exists($ffl_upload_dir)) {
wp_mkdir_p($ffl_upload_dir);
-
- // Create .htaccess to prevent direct access
- $htaccess_content = "Options -IndexesnDeny from alln";
- file_put_contents($ffl_upload_dir . '/.htaccess', $htaccess_content);
}
+
+ $this->maybe_add_directory_protection_files($ffl_upload_dir);
- // Generate unique filename
- $file_extension = pathinfo($file['name'], PATHINFO_EXTENSION);
+ // Generate unique filename using validated extension
+ $file_extension = isset($validation['ext']) ? strtolower($validation['ext']) : '';
+ if (empty($file_extension)) {
+ return array('success' => false, 'message' => 'Invalid file type');
+ }
$unique_filename = 'admin_' . $document_type . '_' . $order_id . '_' . uniqid() . '_' . time() . '.' . $file_extension;
$file_path = $ffl_upload_dir . '/' . $unique_filename;
--- a/g-ffl-checkout/g-ffl-api.php
+++ b/g-ffl-checkout/g-ffl-api.php
@@ -16,7 +16,7 @@
* Plugin Name: g-FFL Checkout
* Plugin URI: garidium.com/g-ffl-api
* Description: g-FFL Checkout
- * Version: 2.1.0
+ * Version: 2.1.1
* WC requires at least: 3.0.0
* WC tested up to: 8.7.0
* Author: Garidium LLC
@@ -47,7 +47,7 @@
* Start at version 1.0.0 and use SemVer - https://semver.org
* Rename this for your plugin and update it as you release new versions.
*/
-define('G_FFL_API_VERSION', '2.1.0');
+define('G_FFL_API_VERSION', '2.1.1');
/**
* The code that runs during plugin activation.
--- a/g-ffl-checkout/includes/ffl_ordering.php
+++ b/g-ffl-checkout/includes/ffl_ordering.php
@@ -2873,7 +2873,7 @@
// Only load on checkout and cart pages
if (is_checkout() || is_cart()) {
// Your existing script enqueue (if not already done elsewhere)
- wp_enqueue_script('ffl-api-public', plugin_dir_url(__FILE__) . '../public/js/ffl-api-public.js', array('jquery'), '2.1.0', true);
+ wp_enqueue_script('ffl-api-public', plugin_dir_url(__FILE__) . '../public/js/ffl-api-public.js', array('jquery'), '2.1.1', true);
// Get current cart data
$has_ammo = cart_contains_ammunition();
--- a/g-ffl-checkout/public/class-ffl-api-public.php
+++ b/g-ffl-checkout/public/class-ffl-api-public.php
@@ -446,6 +446,12 @@
if (function_exists('order_requires_ffl_selector')) {
$requiresFflSelector = order_requires_ffl_selector();
}
+
+ // C&R override should always suppress the FFL selector/map.
+ // (The canonical selector check may not account for this cookie.)
+ if (isset($_COOKIE["g_ffl_checkout_candr_override"])) {
+ $requiresFflSelector = false;
+ }
// Determine where to inject FFL selector
if ($requiresFflSelector) {
@@ -1360,7 +1366,7 @@
Upload required documents for your order. Files will be securely stored and accessible to administrators.
</p>
<p class="ffl-document-formats">
- <em>Supported formats: PDF, JPEG, PNG, GIF (max 10MB each)</em>
+ <em>Supported formats: PDF, JPG/JPEG, PNG, GIF, WebP (max 10MB each)</em>
</p>
</div>
<div class="ffl-document-grid">
@@ -1368,7 +1374,7 @@
<div class="ffl-document-item" data-type="ffl_license">
<div class="ffl-document-label">FFL</div>
<div class="ffl-document-controls">
- <input type="file" class="ffl-document-file-input" data-document-type="ffl_license" accept=".pdf,.jpg,.jpeg,.png,.gif" style="display: none;">
+ <input type="file" class="ffl-document-file-input" data-document-type="ffl_license" accept="application/pdf,image/*" style="display: none;">
<button type="button" class="ffl-upload-button" aria-label="Choose File">
<svg class="ffl-upload-icon" width="20" height="20" viewBox="0 0 20 20" aria-hidden="true" focusable="false" style="display:inline-block;vertical-align:middle;">
<!-- Modern upload icon: arrow up into a tray -->
@@ -1395,7 +1401,7 @@
<div class="ffl-document-item" data-type="sot_license">
<div class="ffl-document-label">SOT</div>
<div class="ffl-document-controls">
- <input type="file" class="ffl-document-file-input" data-document-type="sot_license" accept=".pdf,.jpg,.jpeg,.png,.gif" style="display: none;">
+ <input type="file" class="ffl-document-file-input" data-document-type="sot_license" accept="application/pdf,image/*" style="display: none;">
<button type="button" class="ffl-upload-button" aria-label="Choose File">
<svg class="ffl-upload-icon" width="20" height="20" viewBox="0 0 20 20" aria-hidden="true" focusable="false" style="display:inline-block;vertical-align:middle;">
<!-- Modern upload icon: arrow up into a tray -->
@@ -1422,7 +1428,7 @@
<div class="ffl-document-item<?php echo $state_license_required ? ' required ffl-required-document' : ''; ?>" data-type="state_license">
<div class="ffl-document-label">Firearms License (FOID/FID) <span class="ffl-state-required-indicator" style="display: <?php echo $state_license_required ? 'inline' : 'none'; ?>;">(Required)</span></div>
<div class="ffl-document-controls">
- <input type="file" class="ffl-document-file-input" data-document-type="state_license" accept=".pdf,.jpg,.jpeg,.png,.gif" style="display: none;">
+ <input type="file" class="ffl-document-file-input" data-document-type="state_license" accept="application/pdf,image/*" style="display: none;">
<button type="button" class="ffl-upload-button" aria-label="Choose File">
<svg class="ffl-upload-icon" width="20" height="20" viewBox="0 0 20 20" aria-hidden="true" focusable="false" style="display:inline-block;vertical-align:middle;">
<!-- Modern upload icon: arrow up into a tray -->
@@ -1449,7 +1455,7 @@
<div class="ffl-document-item<?php echo $drivers_license_required ? ' required ffl-required-document' : ''; ?>" data-type="drivers_license">
<div class="ffl-document-label">Driver's License <span class="ffl-state-required-indicator" style="display: <?php echo $drivers_license_required ? 'inline' : 'none'; ?>;">(Required)</span></div>
<div class="ffl-document-controls">
- <input type="file" class="ffl-document-file-input" data-document-type="drivers_license" accept=".pdf,.jpg,.jpeg,.png,.gif" style="display: none;">
+ <input type="file" class="ffl-document-file-input" data-document-type="drivers_license" accept="application/pdf,image/*" style="display: none;">
<button type="button" class="ffl-upload-button" aria-label="Choose File">
<svg class="ffl-upload-icon" width="20" height="20" viewBox="0 0 20 20" aria-hidden="true" focusable="false" style="display:inline-block;vertical-align:middle;">
<!-- Modern upload icon: arrow up into a tray -->
@@ -1858,7 +1864,7 @@
}
// Upload file
- $upload_result = $this->upload_document($file, $document_type);
+ $upload_result = $this->upload_document($file, $document_type, $validation);
if (!$upload_result['success']) {
wp_send_json_error(array('message' => $upload_result['message']));
}
@@ -2082,36 +2088,49 @@
if ($file['size'] > $max_size) {
return array('valid' => false, 'message' => 'File size must be less than 10MB');
}
-
- // Check file type
- $allowed_types = array('application/pdf', 'image/jpeg', 'image/jpg', 'image/png', 'image/gif');
- $file_type = $file['type'];
-
- if (!in_array($file_type, $allowed_types)) {
- return array('valid' => false, 'message' => 'Only PDF, JPEG, PNG, and GIF files are allowed');
+
+ if (empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
+ return array('valid' => false, 'message' => 'Invalid upload');
}
-
- return array('valid' => true);
+
+ // Verify file type using WordPress sniffing (do not trust browser-provided MIME)
+ $allowed_mimes = array(
+ 'pdf' => 'application/pdf',
+ 'jpg' => 'image/jpeg',
+ 'jpeg' => 'image/jpeg',
+ 'png' => 'image/png',
+ 'gif' => 'image/gif',
+ 'webp' => 'image/webp',
+ );
+
+ $filetype = wp_check_filetype_and_ext($file['tmp_name'], $file['name'], $allowed_mimes);
+
+ if (empty($filetype) || empty($filetype['ext']) || empty($filetype['type'])) {
+ return array('valid' => false, 'message' => 'Only PDF and image files (JPG, PNG, GIF, WebP) are allowed');
+ }
+
+ return array('valid' => true, 'ext' => $filetype['ext'], 'type' => $filetype['type']);
}
/**
* Upload document to secure directory
*/
- private function upload_document($file, $document_type) {
+ private function upload_document($file, $document_type, $validation) {
// Create secure upload directory if it doesn't exist
$upload_dir = wp_upload_dir();
$ffl_upload_dir = $upload_dir['basedir'] . '/ffl-documents';
if (!file_exists($ffl_upload_dir)) {
wp_mkdir_p($ffl_upload_dir);
-
- // Create .htaccess to prevent direct access
- $htaccess_content = "Options -IndexesnDeny from alln";
- file_put_contents($ffl_upload_dir . '/.htaccess', $htaccess_content);
}
+
+ $this->maybe_add_directory_protection_files($ffl_upload_dir);
- // Generate unique filename
- $file_extension = pathinfo($file['name'], PATHINFO_EXTENSION);
+ // Generate unique filename using validated extension
+ $file_extension = isset($validation['ext']) ? strtolower($validation['ext']) : '';
+ if (empty($file_extension)) {
+ return array('success' => false, 'message' => 'Invalid file type');
+ }
$unique_filename = $document_type . '_' . uniqid() . '_' . time() . '.' . $file_extension;
$file_path = $ffl_upload_dir . '/' . $unique_filename;
@@ -2131,6 +2150,58 @@
return array('success' => false, 'message' => 'Failed to save file');
}
+
+ /**
+ * Best-effort protection files for upload directories.
+ *
+ * Documents are served via authenticated download handlers; these files are
+ * defense-in-depth and must not break sites on servers that ignore or
+ * restrict per-directory config (e.g., Nginx, IIS, or Apache with limited AllowOverride).
+ */
+ private function maybe_add_directory_protection_files($dir_path) {
+ if (empty($dir_path) || !is_dir($dir_path) || !is_writable($dir_path)) {
+ return;
+ }
+
+ $dir_path = rtrim($dir_path, '/');
+
+ // Prevent directory listing on many servers
+ $index_path = $dir_path . '/index.php';
+ if (!file_exists($index_path)) {
+ @file_put_contents($index_path, "<?phpn// Silence is golden.n");
+ }
+
+ $server_software = isset($_SERVER['SERVER_SOFTWARE']) ? strtolower((string) $_SERVER['SERVER_SOFTWARE']) : '';
+ $looks_like_apache = (strpos($server_software, 'apache') !== false) || (strpos($server_software, 'litespeed') !== false);
+ $looks_like_iis = (strpos($server_software, 'microsoft-iis') !== false) || (strpos($server_software, 'iis') !== false);
+
+ // Apache/LiteSpeed: add an .htaccess deny file when likely supported
+ $htaccess_path = $dir_path . '/.htaccess';
+ if ($looks_like_apache && !file_exists($htaccess_path)) {
+ $htaccess_content = "# Prevent direct web access to uploaded documents.n";
+ $htaccess_content .= "<IfModule mod_authz_core.c>n";
+ $htaccess_content .= "Require all deniedn";
+ $htaccess_content .= "</IfModule>n";
+ $htaccess_content .= "<IfModule !mod_authz_core.c>n";
+ $htaccess_content .= "Deny from alln";
+ $htaccess_content .= "</IfModule>n";
+ @file_put_contents($htaccess_path, $htaccess_content);
+ }
+
+ // IIS: add web.config deny file when applicable
+ $web_config_path = $dir_path . '/web.config';
+ if ($looks_like_iis && !file_exists($web_config_path)) {
+ $web_config_content = "<?xml version="1.0" encoding="UTF-8"?>n";
+ $web_config_content .= "<configuration>n";
+ $web_config_content .= " <system.webServer>n";
+ $web_config_content .= " <authorization>n";
+ $web_config_content .= " <deny users="*" />n";
+ $web_config_content .= " </authorization>n";
+ $web_config_content .= " </system.webServer>n";
+ $web_config_content .= "</configuration>n";
+ @file_put_contents($web_config_path, $web_config_content);
+ }
+ }
/**
* Validate required documents during checkout