4 Commits

Author SHA1 Message Date
2d6bfa219a Release v0.7.1 - Bug Fixes & Client Compatibility
## Fixed
- CRITICAL: Fixed API Verification Secret not displayed in PHP fallback template
- Response signing now includes /update-check endpoint

## Changed
- Updated magdev/wc-licensed-product-client to v0.2.2
- Updated symfony/http-client to v7.4.5

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 12:07:23 +01:00
302f2e76ca Update translations for v0.7.1
- Regenerated .pot template with 388 strings
- All German (de_CH) translations up to date
- Compiled .mo file for production

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 12:06:45 +01:00
5938aaed1b 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>
2026-01-28 11:38:59 +01:00
630a5859d3 Update CLAUDE.md with v0.7.0 security documentation
- Updated Security Best Practices section with v0.7.0 security measures
- Cleared Temporary Roadmap (v0.7.0 completed)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 11:35:56 +01:00
11 changed files with 3647 additions and 3461 deletions

View File

@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.7.1] - 2026-01-28
### Fixed
- **CRITICAL:** Fixed API Verification Secret not displayed in PHP fallback template on customer account licenses page
- Response signing now includes `/update-check` endpoint (was missing from signed routes)
### Changed
- Updated `magdev/wc-licensed-product-client` dependency to v0.2.2
- Updated `symfony/http-client` dependency to v7.4.5
### Technical Details
- Added customer secret display to `displayLicensesFallback()` method in `AccountController`
- Added `/update-check` route to `ResponseSigner::shouldSign()` method for consistent signature headers
- Verified server implementation aligns with updated client library documentation
## [0.7.0] - 2026-01-28 ## [0.7.0] - 2026-01-28
### Security ### Security

View File

@@ -32,14 +32,9 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file. **Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
### Version 0.7.0 ### Version 0.7.2
This is a security version. It includes a full security audit and a remote check of a live version of this plugin on <https://shop.magdev.cc>. The shop is the property of the plugin developer, all actions are permitted. No pending features.
- Check the sourcecode for best practises of all involved components, including checks for SQLi, XSRF, XSS and similar techniques
- Check the remote version for the OWASP Top 10
- Check the whole licensing workflow
- Minimize the thread vectors
## Technical Stack ## Technical Stack
@@ -60,6 +55,13 @@ This is a security version. It includes a full security audit and a remote check
- Nonce verification on form submissions - Nonce verification on form submissions
- Output escaping in templates (`esc_attr`, `esc_html`, `esc_js`) - Output escaping in templates (`esc_attr`, `esc_html`, `esc_js`)
- Direct file access prevention via `ABSPATH` check - Direct file access prevention via `ABSPATH` check
- XSS-safe DOM construction in JavaScript (no `innerHTML` with user data)
- Rate limiting on API endpoints (configurable via `WC_LICENSE_RATE_LIMIT`)
- Rate limiting on frontend operations (transfers: 5/hour, downloads: 30/hour)
- CSV import limits (2MB max, 1000 rows max, 5-minute cooldown)
- IP detection with proxy support via `IpDetectionTrait` (supports `WC_LICENSE_TRUSTED_PROXIES`)
- SQL injection prevention using `$wpdb->prepare()` throughout
- Secure download URLs with hash verification using `hash_equals()`
### Translation Ready ### Translation Ready
@@ -1843,3 +1845,30 @@ Security-focused release with comprehensive audit and hardening. Performed OWASP
- Created release package: `releases/wc-licensed-product-0.7.0.zip` (883 KB) - Created release package: `releases/wc-licensed-product-0.7.0.zip` (883 KB)
- SHA256: `12f8452316e350273003f36bf6d7b7121a7bedc9a6964c3d0732d26318d94c18` - SHA256: `12f8452316e350273003f36bf6d7b7121a7bedc9a6964c3d0732d26318d94c18`
- Tagged as `v0.7.0` and pushed to `main` branch - Tagged as `v0.7.0` and pushed to `main` branch
### 2026-01-28 - Version 0.7.1 - Bug Fixes & Client Compatibility
**Overview:**
Bug fix release ensuring compatibility with updated `magdev/wc-licensed-product-client` v0.2.2 and fixing API Verification Secret display.
**Bug Fixes:**
- **CRITICAL:** Fixed API Verification Secret not displaying on customer account licenses page when using PHP fallback (Twig unavailable)
- Fixed `/update-check` endpoint responses not being signed (missing from `ResponseSigner::shouldSign()`)
**Dependency Updates:**
- Updated `magdev/wc-licensed-product-client` from `760e1e7` to `56abe8a` (v0.2.2)
- Updated `symfony/http-client` from v7.4.4 to v7.4.5
**Modified files:**
- `src/Frontend/AccountController.php` - Added customer secret display to PHP fallback method `displayLicensesFallback()`
- `src/Api/ResponseSigner.php` - Added `/update-check` to `shouldSign()` method
**Technical notes:**
- PHP fallback template now includes the collapsible API Verification Secret section matching the Twig template
- All four API endpoints (`/validate`, `/status`, `/activate`, `/update-check`) now include signature headers when `WC_LICENSE_SERVER_SECRET` is configured
- Client library v0.2.2 verified compatible with server implementation

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

16
composer.lock generated
View File

@@ -12,7 +12,7 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git", "url": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git",
"reference": "760e1e752a0c088fa634cf7ff678e0735ed525a4" "reference": "56abe8a97c72419c07a6daf263ba6f4a9b5fe4b1"
}, },
"require": { "require": {
"php": "^8.3", "php": "^8.3",
@@ -52,7 +52,7 @@
"issues": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client/issues", "issues": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client/issues",
"source": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client" "source": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client"
}, },
"time": "2026-01-27T19:52:12+00:00" "time": "2026-01-28T10:56:47+00:00"
}, },
{ {
"name": "psr/cache", "name": "psr/cache",
@@ -380,16 +380,16 @@
}, },
{ {
"name": "symfony/http-client", "name": "symfony/http-client",
"version": "v7.4.4", "version": "v7.4.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/http-client.git", "url": "https://github.com/symfony/http-client.git",
"reference": "d63c23357d74715a589454c141c843f0172bec6c" "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/d63c23357d74715a589454c141c843f0172bec6c", "url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f",
"reference": "d63c23357d74715a589454c141c843f0172bec6c", "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -457,7 +457,7 @@
"http" "http"
], ],
"support": { "support": {
"source": "https://github.com/symfony/http-client/tree/v7.4.4" "source": "https://github.com/symfony/http-client/tree/v7.4.5"
}, },
"funding": [ "funding": [
{ {
@@ -477,7 +477,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-01-23T16:34:22+00:00" "time": "2026-01-27T16:16:02+00:00"
}, },
{ {
"name": "symfony/http-client-contracts", "name": "symfony/http-client-contracts",

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -79,7 +79,8 @@ final class ResponseSigner
return str_starts_with($route, '/wc-licensed-product/v1/validate') return str_starts_with($route, '/wc-licensed-product/v1/validate')
|| str_starts_with($route, '/wc-licensed-product/v1/status') || str_starts_with($route, '/wc-licensed-product/v1/status')
|| str_starts_with($route, '/wc-licensed-product/v1/activate'); || str_starts_with($route, '/wc-licensed-product/v1/activate')
|| str_starts_with($route, '/wc-licensed-product/v1/update-check');
} }
/** /**

View File

@@ -428,6 +428,26 @@ final class AccountController
?> ?>
</span> </span>
</div> </div>
<?php if (ResponseSigner::isSigningEnabled() && !empty($license['customer_secret'])): ?>
<div class="license-row-secret">
<button type="button" class="secret-toggle" aria-expanded="false">
<span class="dashicons dashicons-lock"></span>
<?php esc_html_e('API Verification Secret', 'wc-licensed-product'); ?>
<span class="dashicons dashicons-arrow-down-alt2 toggle-arrow"></span>
</button>
<div class="secret-content" style="display: none;">
<p class="secret-description">
<?php esc_html_e('Use this secret to verify signed API responses. Keep it secure.', 'wc-licensed-product'); ?>
</p>
<div class="secret-value-wrapper">
<code class="secret-value"><?php echo esc_html($license['customer_secret']); ?></code>
<button type="button" class="copy-secret-btn" data-secret="<?php echo esc_attr($license['customer_secret']); ?>" title="<?php esc_attr_e('Copy to clipboard', 'wc-licensed-product'); ?>">
<span class="dashicons dashicons-clipboard"></span>
</button>
</div>
</div>
</div>
<?php endif; ?>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>

View File

@@ -3,7 +3,7 @@
* Plugin Name: WooCommerce Licensed Product * Plugin Name: WooCommerce Licensed Product
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product * Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product
* Description: WooCommerce plugin to sell software products using license keys with domain-based validation. * Description: WooCommerce plugin to sell software products using license keys with domain-based validation.
* Version: 0.7.0 * Version: 0.7.1
* Author: Marco Graetsch * Author: Marco Graetsch
* Author URI: https://src.bundespruefstelle.ch/magdev * Author URI: https://src.bundespruefstelle.ch/magdev
* License: GPL-2.0-or-later * License: GPL-2.0-or-later
@@ -28,7 +28,7 @@ if (!defined('ABSPATH')) {
} }
// Plugin constants // Plugin constants
define('WC_LICENSED_PRODUCT_VERSION', '0.7.0'); define('WC_LICENSED_PRODUCT_VERSION', '0.7.1');
define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__); define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__);
define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__)); define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));