diff --git a/README.md b/README.md index 187b1e9..2a1ae62 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ WC Licensed Product adds a new product type "Licensed Product" to WooCommerce, e - **Version Binding**: Optional binding to major software versions - **Expiration Support**: Set license validity periods or lifetime licenses - **Rate Limiting**: API endpoints protected with configurable rate limiting (default: 30 requests/minute) +- **Frontend Rate Limiting**: Transfer requests (5/hour) and downloads (30/hour) protected against abuse - **Trusted Proxy Support**: Configurable trusted proxies for accurate rate limiting behind CDNs - **Checkout Blocks**: Full support for WooCommerce Checkout Blocks (default since WC 8.3+) - **Self-Licensing**: The plugin can validate its own license (for commercial distribution) @@ -132,17 +133,26 @@ When a customer purchases a licensed product, they must enter the domain where t 3. Upload a CSV file (supports exported format or simplified format) 4. Choose options: skip header row, update existing licenses +**Import Limits (Security):** + +- Maximum file size: 2MB +- Maximum rows per import: 1000 +- Cooldown between imports: 5 minutes + ## Security The plugin implements several security best practices: - **Input Sanitization**: All user inputs are sanitized using WordPress functions - **Output Escaping**: All output is escaped to prevent XSS attacks +- **XSS-Safe DOM Construction**: JavaScript uses `createElement()` and `textContent` instead of `innerHTML` - **CSRF Protection**: Nonce verification on all forms and AJAX requests - **SQL Injection Prevention**: All database queries use prepared statements - **Capability Checks**: Admin functions require `manage_woocommerce` capability - **Secure Downloads**: File downloads use hash-verified URLs with user authentication - **Response Signing**: Optional HMAC-SHA256 signatures for API tamper protection +- **Rate Limiting**: API and frontend operations protected against abuse +- **Import Limits**: CSV imports limited by file size, row count, and cooldown period ### Trusted Proxy Configuration diff --git a/docs/server-implementation.md b/docs/server-implementation.md index 71add02..4e61e96 100644 --- a/docs/server-implementation.md +++ b/docs/server-implementation.md @@ -21,7 +21,7 @@ This prevents attackers from: ## Requirements -- PHP 7.4+ (8.0+ recommended) +- PHP 8.3+ - A server secret stored securely (not in version control) ## Server Configuration @@ -51,25 +51,33 @@ php -r "echo bin2hex(random_bytes(32));" ### Key Derivation -Each license key gets a unique signing key derived from the server secret: +Each license key gets a unique signing key derived from the server secret using RFC 5869 HKDF: ```php /** * Derive a unique signing key for a license. * - * @param string $licenseKey The license key - * @param string $serverSecret The server's master secret - * @return string The derived key (hex encoded) + * Uses PHP's native hash_hkdf() function per RFC 5869. + * + * @param string $licenseKey The license key (used as "info" context) + * @param string $serverSecret The server's master secret (used as IKM) + * @return string The derived key (hex encoded, 64 characters) */ function derive_signing_key(string $licenseKey, string $serverSecret): string { - // HKDF-like key derivation - $prk = hash_hmac('sha256', $licenseKey, $serverSecret, true); - - return hash_hmac('sha256', $prk . "\x01", $serverSecret); + // HKDF key derivation per RFC 5869 + // IKM: server_secret, Length: 32 bytes, Info: license_key + return bin2hex(hash_hkdf('sha256', $serverSecret, 32, $licenseKey)); } ``` +**Important:** This uses PHP's native `hash_hkdf()` function (available since PHP 7.1.2). The parameters are: + +- **Algorithm:** sha256 +- **IKM (Input Keying Material):** server_secret +- **Length:** 32 bytes (256 bits) +- **Info:** license_key (context-specific information) + ### Response Signing Sign every API response before sending: @@ -88,8 +96,8 @@ function sign_response(array $responseData, string $licenseKey, string $serverSe $timestamp = time(); $signingKey = derive_signing_key($licenseKey, $serverSecret); - // Sort keys for consistent ordering - ksort($responseData); + // Recursively sort keys for consistent ordering (important for nested arrays!) + $responseData = recursive_key_sort($responseData); // Build signature payload $jsonBody = json_encode($responseData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); @@ -103,6 +111,20 @@ function sign_response(array $responseData, string $licenseKey, string $serverSe 'X-License-Timestamp' => (string) $timestamp, ]; } + +/** + * Recursively sort array keys alphabetically. + */ +function recursive_key_sort(array $data): array +{ + ksort($data); + foreach ($data as $key => $value) { + if (is_array($value)) { + $data[$key] = recursive_key_sort($value); + } + } + return $data; +} ``` ### WordPress REST API Integration @@ -214,7 +236,7 @@ class ResponseSigner $timestamp = time(); $signingKey = $this->deriveKey($licenseKey); - ksort($data); + $data = $this->recursiveKeySort($data); $payload = $timestamp . ':' . json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE @@ -226,11 +248,21 @@ class ResponseSigner ]; } + private function recursiveKeySort(array $data): array + { + ksort($data); + foreach ($data as $key => $value) { + if (is_array($value)) { + $data[$key] = $this->recursiveKeySort($value); + } + } + return $data; + } + private function deriveKey(string $licenseKey): string { - $prk = hash_hmac('sha256', $licenseKey, $this->serverSecret, true); - - return hash_hmac('sha256', $prk . "\x01", $this->serverSecret); + // HKDF key derivation per RFC 5869 + return bin2hex(hash_hkdf('sha256', $this->serverSecret, 32, $licenseKey)); } } @@ -262,8 +294,8 @@ signature = HMAC-SHA256( Where: -- `derive_signing_key` uses HKDF-like derivation (see above) -- `canonical_json` sorts keys alphabetically, no escaping of slashes/unicode +- `derive_signing_key` uses RFC 5869 HKDF: `hash_hkdf('sha256', server_secret, 32, license_key)` +- `canonical_json` recursively sorts keys alphabetically, no escaping of slashes/unicode - Result is hex-encoded (64 characters) ## Testing