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:
91
src/Common/RateLimitTrait.php
Normal file
91
src/Common/RateLimitTrait.php
Normal 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user