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:
2026-01-28 11:27:08 +01:00
parent d0af939f5e
commit b50969f701
12 changed files with 500 additions and 211 deletions

View File

@@ -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>