GHSA-95JH-7R58-XMXW

Vulnerability from github – Published: 2026-06-22 19:58 – Updated: 2026-06-22 19:58
VLAI
Summary
AVideo has an Authorize.Net Webhook Signature Bypass that Enables Wallet Balance Inflation via Forged Payment Data
Details

Summary

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.

Details

Three flaws combine into an exploit chain:

1. Signature Bypass via OR Logic (webhook.php:33)

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.

2. Payload Values Override API-Fetched Values (AuthorizeNet.php:169-171, webhook.php:44-48)

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.

3. Missing Approval Check (webhook.php:61-75)

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.

PoC

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., 60123456789).

  1. Immediately after the purchase completes (to race the legitimate webhook), send a forged webhook:
curl -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
      }
    }
  }'
  1. 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.

  2. analyzeTransactionFromWebhook() uses the forged payload's amount: 99999.99 and metadata.users_id: 2.

  3. processSinglePayment() credits $99,999.99 to user ID 2's wallet via addBalance().

  4. The dedup key is sha1('net.authorize.payment.authcapture.created' . '60123456789'), so the legitimate webhook arriving later is silently discarded as a duplicate.

  5. The attacker can repeat with new transaction IDs from additional small purchases for cumulative balance inflation.

Impact

  • Wallet balance inflation: Attacker credits arbitrary amounts to any user's wallet without corresponding payment, bypassing the payment gateway's actual charge amount.
  • Premium content access: Inflated wallet balance allows purchasing all paid/premium video content without real payment.
  • Subscription fraud: By including plans_id in forged metadata, the attacker can activate premium subscriptions (webhook.php:86-134) without corresponding payment.
  • Financial loss: Platform owner loses revenue from fraudulently accessed premium content and services.

Recommended Fix

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;
}
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 28.0"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "wwbn/avideo"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "29.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-33731"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-345"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-22T19:58:50Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nThe 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.\n\n## Details\n\nThree flaws combine into an exploit chain:\n\n### 1. Signature Bypass via OR Logic (webhook.php:33)\n\n```php\nif (!$parsed[\u0027signatureValid\u0027] \u0026\u0026 (empty($txnInfo) || !empty($txnInfo[\u0027error\u0027]))) {\n    http_response_code(401);\n    echo \u0027invalid signature\u0027;\n    exit;\n}\n```\n\nThe 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.\n\n### 2. Payload Values Override API-Fetched Values (AuthorizeNet.php:169-171, webhook.php:44-48)\n\nIn `analyzeTransactionFromWebhook()`, `users_id` and `amount` are extracted from the attacker-controlled webhook **payload** first:\n\n```php\n$users_id = isset($metadata[\u0027users_id\u0027]) ? (int)$metadata[\u0027users_id\u0027] : null;\n$amount   = isset($payload[\u0027amount\u0027]) ? (float)$payload[\u0027amount\u0027] : ...;\n```\n\nThe fallback logic in webhook.php only applies when the analysis values are empty/falsy:\n\n```php\nif (!$analysis[\u0027users_id\u0027] \u0026\u0026 !empty($txnInfo[\u0027users_id\u0027])) {\n    $analysis[\u0027users_id\u0027] = (int)$txnInfo[\u0027users_id\u0027];\n}\nif (!$analysis[\u0027amount\u0027] \u0026\u0026 isset($txnInfo[\u0027amount\u0027])) {\n    $analysis[\u0027amount\u0027] = (float)$txnInfo[\u0027amount\u0027];\n}\n```\n\nSince the forged payload already provides both values, the authoritative API-fetched values are never used.\n\n### 3. Missing Approval Check (webhook.php:61-75)\n\nThe 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.\n\n## PoC\n\n**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., `60123456789`).\n\n1. Immediately after the purchase completes (to race the legitimate webhook), send a forged webhook:\n\n```bash\ncurl -X POST https://target.com/plugin/AuthorizeNet/webhook.php \\\n  -H \u0027Content-Type: application/json\u0027 \\\n  -d \u0027{\n    \"eventType\": \"net.authorize.payment.authcapture.created\",\n    \"payload\": {\n      \"id\": \"60123456789\",\n      \"amount\": 99999.99,\n      \"responseCode\": 1,\n      \"metadata\": {\n        \"users_id\": 2\n      }\n    }\n  }\u0027\n```\n\n2. The signature check fails (no `X-ANET-Signature` header), but `getTransactionDetails(\u002760123456789\u0027)` succeeds because it is a real transaction. The OR condition on line 33 is not fully satisfied, so execution continues.\n\n3. `analyzeTransactionFromWebhook()` uses the forged payload\u0027s `amount: 99999.99` and `metadata.users_id: 2`.\n\n4. `processSinglePayment()` credits $99,999.99 to user ID 2\u0027s wallet via `addBalance()`.\n\n5. The dedup key is `sha1(\u0027net.authorize.payment.authcapture.created\u0027 . \u002760123456789\u0027)`, so the legitimate webhook arriving later is silently discarded as a duplicate.\n\n6. The attacker can repeat with new transaction IDs from additional small purchases for cumulative balance inflation.\n\n## Impact\n\n- **Wallet balance inflation:** Attacker credits arbitrary amounts to any user\u0027s wallet without corresponding payment, bypassing the payment gateway\u0027s actual charge amount.\n- **Premium content access:** Inflated wallet balance allows purchasing all paid/premium video content without real payment.\n- **Subscription fraud:** By including `plans_id` in forged metadata, the attacker can activate premium subscriptions (webhook.php:86-134) without corresponding payment.\n- **Financial loss:** Platform owner loses revenue from fraudulently accessed premium content and services.\n\n## Recommended Fix\n\n**1. Reject webhooks with invalid signatures unconditionally** \u2014 the transaction lookup should only be used for data enrichment *after* signature validation passes:\n\n```php\n// webhook.php line 33 \u2014 FIX: reject on invalid signature alone\nif (!$parsed[\u0027signatureValid\u0027]) {\n    _error_log(\u0027[Authorize.Net webhook] Bad signature\u0027);\n    http_response_code(401);\n    echo \u0027invalid signature\u0027;\n    exit;\n}\n```\n\n**2. Use API-fetched values as authoritative** \u2014 in webhook.php lines 44-55, invert the precedence so `$txnInfo` values always override payload values:\n\n```php\n// Always prefer API-fetched values over payload values\nif (!empty($txnInfo[\u0027users_id\u0027])) {\n    $analysis[\u0027users_id\u0027] = (int)$txnInfo[\u0027users_id\u0027];\n}\nif (isset($txnInfo[\u0027amount\u0027])) {\n    $analysis[\u0027amount\u0027] = (float)$txnInfo[\u0027amount\u0027];\n}\n```\n\n**3. Check `isApproved` before processing** \u2014 add a gate before `processSinglePayment()`:\n\n```php\nif (!$analysis[\u0027isApproved\u0027]) {\n    _error_log(\u0027[Authorize.Net webhook] Transaction not approved\u0027);\n    http_response_code(400);\n    echo \u0027transaction not approved\u0027;\n    exit;\n}\n```",
  "id": "GHSA-95jh-7r58-xmxw",
  "modified": "2026-06-22T19:58:51Z",
  "published": "2026-06-22T19:58:50Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-95jh-7r58-xmxw"
    },
    {
      "type": "WEB",
      "url": "https://github.com/WWBN/AVideo/commit/033e83ae904cacb99495dbea7cbcfb3738cf42e4"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/WWBN/AVideo"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "AVideo has an Authorize.Net Webhook Signature Bypass that Enables Wallet Balance Inflation via Forged Payment Data"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…