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

@@ -0,0 +1,91 @@
<?php
/**
* Rate Limit Trait
*
* Provides rate limiting functionality for frontend operations.
*
* @package Jeremias\WcLicensedProduct\Common
*/
declare(strict_types=1);
namespace Jeremias\WcLicensedProduct\Common;
/**
* Trait for implementing rate limiting on user actions
*
* Uses WordPress transients for storage. Rate limits are per-user when logged in,
* or per-IP when not logged in.
*/
trait RateLimitTrait
{
/**
* Check rate limit for a user action
*
* @param string $action Action identifier (e.g., 'transfer', 'download')
* @param int $limit Maximum attempts per window
* @param int $window Time window in seconds
* @return bool True if within limit, false if exceeded
*/
protected function checkUserRateLimit(string $action, int $limit, int $window): bool
{
$userId = get_current_user_id();
$key = $userId > 0
? (string) $userId
: 'ip_' . md5($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0');
$transientKey = 'wclp_rate_' . $action . '_' . $key;
$data = get_transient($transientKey);
if ($data === false) {
// First request, start counting
set_transient($transientKey, ['count' => 1, 'start' => time()], $window);
return true;
}
$count = (int) ($data['count'] ?? 0);
$start = (int) ($data['start'] ?? time());
// Check if window has expired
if (time() - $start >= $window) {
// Reset counter
set_transient($transientKey, ['count' => 1, 'start' => time()], $window);
return true;
}
// Check if limit exceeded
if ($count >= $limit) {
return false;
}
// Increment counter
set_transient($transientKey, ['count' => $count + 1, 'start' => $start], $window);
return true;
}
/**
* Get remaining time until rate limit resets
*
* @param string $action Action identifier
* @param int $window Time window in seconds (must match the one used in checkUserRateLimit)
* @return int Seconds until rate limit resets, or 0 if not rate limited
*/
protected function getRateLimitRetryAfter(string $action, int $window): int
{
$userId = get_current_user_id();
$key = $userId > 0
? (string) $userId
: 'ip_' . md5($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0');
$transientKey = 'wclp_rate_' . $action . '_' . $key;
$data = get_transient($transientKey);
if ($data === false) {
return 0;
}
$start = (int) ($data['start'] ?? time());
return max(0, $window - (time() - $start));
}
}