You've already forked wc-licensed-product
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:
10
README.md
10
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
|
- **Version Binding**: Optional binding to major software versions
|
||||||
- **Expiration Support**: Set license validity periods or lifetime licenses
|
- **Expiration Support**: Set license validity periods or lifetime licenses
|
||||||
- **Rate Limiting**: API endpoints protected with configurable rate limiting (default: 30 requests/minute)
|
- **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
|
- **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+)
|
- **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)
|
- **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)
|
3. Upload a CSV file (supports exported format or simplified format)
|
||||||
4. Choose options: skip header row, update existing licenses
|
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
|
## Security
|
||||||
|
|
||||||
The plugin implements several security best practices:
|
The plugin implements several security best practices:
|
||||||
|
|
||||||
- **Input Sanitization**: All user inputs are sanitized using WordPress functions
|
- **Input Sanitization**: All user inputs are sanitized using WordPress functions
|
||||||
- **Output Escaping**: All output is escaped to prevent XSS attacks
|
- **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
|
- **CSRF Protection**: Nonce verification on all forms and AJAX requests
|
||||||
- **SQL Injection Prevention**: All database queries use prepared statements
|
- **SQL Injection Prevention**: All database queries use prepared statements
|
||||||
- **Capability Checks**: Admin functions require `manage_woocommerce` capability
|
- **Capability Checks**: Admin functions require `manage_woocommerce` capability
|
||||||
- **Secure Downloads**: File downloads use hash-verified URLs with user authentication
|
- **Secure Downloads**: File downloads use hash-verified URLs with user authentication
|
||||||
- **Response Signing**: Optional HMAC-SHA256 signatures for API tamper protection
|
- **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
|
### Trusted Proxy Configuration
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ This prevents attackers from:
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- PHP 7.4+ (8.0+ recommended)
|
- PHP 8.3+
|
||||||
- A server secret stored securely (not in version control)
|
- A server secret stored securely (not in version control)
|
||||||
|
|
||||||
## Server Configuration
|
## Server Configuration
|
||||||
@@ -51,25 +51,33 @@ php -r "echo bin2hex(random_bytes(32));"
|
|||||||
|
|
||||||
### Key Derivation
|
### 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
|
```php
|
||||||
/**
|
/**
|
||||||
* Derive a unique signing key for a license.
|
* Derive a unique signing key for a license.
|
||||||
*
|
*
|
||||||
* @param string $licenseKey The license key
|
* Uses PHP's native hash_hkdf() function per RFC 5869.
|
||||||
* @param string $serverSecret The server's master secret
|
*
|
||||||
* @return string The derived key (hex encoded)
|
* @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
|
function derive_signing_key(string $licenseKey, string $serverSecret): string
|
||||||
{
|
{
|
||||||
// HKDF-like key derivation
|
// HKDF key derivation per RFC 5869
|
||||||
$prk = hash_hmac('sha256', $licenseKey, $serverSecret, true);
|
// IKM: server_secret, Length: 32 bytes, Info: license_key
|
||||||
|
return bin2hex(hash_hkdf('sha256', $serverSecret, 32, $licenseKey));
|
||||||
return hash_hmac('sha256', $prk . "\x01", $serverSecret);
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**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
|
### Response Signing
|
||||||
|
|
||||||
Sign every API response before sending:
|
Sign every API response before sending:
|
||||||
@@ -88,8 +96,8 @@ function sign_response(array $responseData, string $licenseKey, string $serverSe
|
|||||||
$timestamp = time();
|
$timestamp = time();
|
||||||
$signingKey = derive_signing_key($licenseKey, $serverSecret);
|
$signingKey = derive_signing_key($licenseKey, $serverSecret);
|
||||||
|
|
||||||
// Sort keys for consistent ordering
|
// Recursively sort keys for consistent ordering (important for nested arrays!)
|
||||||
ksort($responseData);
|
$responseData = recursive_key_sort($responseData);
|
||||||
|
|
||||||
// Build signature payload
|
// Build signature payload
|
||||||
$jsonBody = json_encode($responseData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
$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,
|
'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
|
### WordPress REST API Integration
|
||||||
@@ -214,7 +236,7 @@ class ResponseSigner
|
|||||||
$timestamp = time();
|
$timestamp = time();
|
||||||
$signingKey = $this->deriveKey($licenseKey);
|
$signingKey = $this->deriveKey($licenseKey);
|
||||||
|
|
||||||
ksort($data);
|
$data = $this->recursiveKeySort($data);
|
||||||
$payload = $timestamp . ':' . json_encode(
|
$payload = $timestamp . ':' . json_encode(
|
||||||
$data,
|
$data,
|
||||||
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
|
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
|
private function deriveKey(string $licenseKey): string
|
||||||
{
|
{
|
||||||
$prk = hash_hmac('sha256', $licenseKey, $this->serverSecret, true);
|
// HKDF key derivation per RFC 5869
|
||||||
|
return bin2hex(hash_hkdf('sha256', $this->serverSecret, 32, $licenseKey));
|
||||||
return hash_hmac('sha256', $prk . "\x01", $this->serverSecret);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,8 +294,8 @@ signature = HMAC-SHA256(
|
|||||||
|
|
||||||
Where:
|
Where:
|
||||||
|
|
||||||
- `derive_signing_key` uses HKDF-like derivation (see above)
|
- `derive_signing_key` uses RFC 5869 HKDF: `hash_hkdf('sha256', server_secret, 32, license_key)`
|
||||||
- `canonical_json` sorts keys alphabetically, no escaping of slashes/unicode
|
- `canonical_json` recursively sorts keys alphabetically, no escaping of slashes/unicode
|
||||||
- Result is hex-encoded (64 characters)
|
- Result is hex-encoded (64 characters)
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|||||||
Reference in New Issue
Block a user