Update documentation for v0.7.0 security features

README.md:
- Added frontend rate limiting info (transfers: 5/hour, downloads: 30/hour)
- Added CSV import limits section (2MB, 1000 rows, 5-min cooldown)
- Added XSS-safe DOM construction to security section
- Added rate limiting and import limits to security best practices

docs/server-implementation.md:
- Updated PHP requirement to 8.3+
- Fixed key derivation to use RFC 5869 hash_hkdf() (v0.5.5 fix)
- Added recursive key sorting for signature generation
- Updated signature algorithm documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-28 11:38:59 +01:00
parent 630a5859d3
commit 5938aaed1b
2 changed files with 59 additions and 17 deletions

View File

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

View File

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