You've already forked wc-licensed-product
Release v0.7.0 - Security Hardening
Security Fixes: - Fixed XSS vulnerability in checkout blocks DOM injection (replaced innerHTML with safe DOM methods) - Unified IP detection for rate limiting across all API endpoints (new IpDetectionTrait) - Added rate limiting to license transfers (5/hour) and downloads (30/hour) (new RateLimitTrait) - Added file size limit (2MB), row limit (1000), and rate limiting to CSV import - Added JSON decode error handling in StoreApiExtension - Added license ID validation in frontend.js to prevent selector injection New Files: - src/Api/IpDetectionTrait.php - Shared IP detection with proxy support - src/Common/RateLimitTrait.php - Reusable rate limiting for frontend operations Breaking Changes: - None Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,21 @@ use Twig\Environment;
|
||||
*/
|
||||
final class AdminController
|
||||
{
|
||||
/**
|
||||
* Maximum CSV file size in bytes (2MB)
|
||||
*/
|
||||
private const MAX_IMPORT_FILE_SIZE = 2 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Maximum rows to import per file
|
||||
*/
|
||||
private const MAX_IMPORT_ROWS = 1000;
|
||||
|
||||
/**
|
||||
* Minimum time between imports in seconds (5 minutes)
|
||||
*/
|
||||
private const IMPORT_RATE_LIMIT_WINDOW = 300;
|
||||
|
||||
private Environment $twig;
|
||||
private LicenseManager $licenseManager;
|
||||
|
||||
@@ -653,6 +668,23 @@ final class AdminController
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check file size limit
|
||||
if ($file['size'] > self::MAX_IMPORT_FILE_SIZE) {
|
||||
wp_redirect(admin_url('admin.php?page=wc-licenses&action=import_csv&import_error=size'));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check rate limit for imports
|
||||
$lastImport = get_transient('wclp_last_csv_import_' . get_current_user_id());
|
||||
if ($lastImport !== false && (time() - $lastImport) < self::IMPORT_RATE_LIMIT_WINDOW) {
|
||||
$retryAfter = self::IMPORT_RATE_LIMIT_WINDOW - (time() - $lastImport);
|
||||
wp_redirect(admin_url('admin.php?page=wc-licenses&action=import_csv&import_error=rate_limit&retry_after=' . $retryAfter));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Set rate limit marker
|
||||
set_transient('wclp_last_csv_import_' . get_current_user_id(), time(), self::IMPORT_RATE_LIMIT_WINDOW);
|
||||
|
||||
// Read the CSV file
|
||||
$handle = fopen($file['tmp_name'], 'r');
|
||||
if (!$handle) {
|
||||
@@ -679,6 +711,7 @@ final class AdminController
|
||||
$updated = 0;
|
||||
$skipped = 0;
|
||||
$errors = [];
|
||||
$rowCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle)) !== false) {
|
||||
// Skip empty rows
|
||||
@@ -686,6 +719,24 @@ final class AdminController
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check row limit
|
||||
$rowCount++;
|
||||
if ($rowCount > self::MAX_IMPORT_ROWS) {
|
||||
fclose($handle);
|
||||
$this->addNotice(
|
||||
sprintf(
|
||||
/* translators: %1$d: max rows, %2$d: imported count, %3$d: updated count */
|
||||
__('Import stopped: Maximum of %1$d rows allowed. %2$d imported, %3$d updated.', 'wc-licensed-product'),
|
||||
self::MAX_IMPORT_ROWS,
|
||||
$imported,
|
||||
$updated
|
||||
),
|
||||
'warning'
|
||||
);
|
||||
wp_redirect(admin_url('admin.php?page=wc-licenses&import_success=partial'));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Map CSV columns (expected format from export):
|
||||
// ID, License Key, Product, Product ID, Order ID, Order Number, Customer, Customer Email, Customer ID, Domain, Status, Activations, Max Activations, Expires At, Created At, Updated At
|
||||
// For import we need: License Key (or generate), Product ID, Customer ID, Domain, Status, Max Activations, Expires At
|
||||
@@ -1700,6 +1751,21 @@ final class AdminController
|
||||
case 'read':
|
||||
esc_html_e('Error reading file. Please check the file format.', 'wc-licensed-product');
|
||||
break;
|
||||
case 'size':
|
||||
printf(
|
||||
/* translators: %s: max file size */
|
||||
esc_html__('File too large. Maximum size is %s.', 'wc-licensed-product'),
|
||||
esc_html(size_format(self::MAX_IMPORT_FILE_SIZE))
|
||||
);
|
||||
break;
|
||||
case 'rate_limit':
|
||||
$retryAfter = isset($_GET['retry_after']) ? absint($_GET['retry_after']) : self::IMPORT_RATE_LIMIT_WINDOW;
|
||||
printf(
|
||||
/* translators: %d: seconds to wait */
|
||||
esc_html__('Please wait %d seconds before importing again.', 'wc-licensed-product'),
|
||||
$retryAfter
|
||||
);
|
||||
break;
|
||||
default:
|
||||
esc_html_e('An error occurred during import.', 'wc-licensed-product');
|
||||
}
|
||||
@@ -1708,6 +1774,20 @@ final class AdminController
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="notice notice-info" style="max-width: 800px;">
|
||||
<p>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %1$s: max file size, %2$d: max rows, %3$d: rate limit minutes */
|
||||
esc_html__('Import limits: Maximum file size %1$s, maximum %2$d rows per import. You can import again after %3$d minutes.', 'wc-licensed-product'),
|
||||
esc_html(size_format(self::MAX_IMPORT_FILE_SIZE)),
|
||||
self::MAX_IMPORT_ROWS,
|
||||
(int) (self::IMPORT_RATE_LIMIT_WINDOW / 60)
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card" style="max-width: 800px; padding: 20px;">
|
||||
<h2><?php esc_html_e('Import Licenses from CSV', 'wc-licensed-product'); ?></h2>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user