You've already forked wc-licensed-product
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>
92 lines
2.6 KiB
PHP
92 lines
2.6 KiB
PHP
<?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));
|
|
}
|
|
}
|