GHSA-95jh-7r58-xmxw is a medium-severity (CVSS 6.5) CWE-345 vulnerability in wwbn/avideo. A fix is available for wwbn/avideo — see the affected versions and patch details below.
AVideo has an Authorize.Net Webhook Signature Bypass that Enables Wallet Balance Inflation via Forged Payment Data
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
Exploitation and automatability from CISA’s SSVC triage for GHSA-95jh-7r58-xmxw.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
GHSA-95jh-7r58-xmxw plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 376,715 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
wwbn/avideoReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Packagist packages — download data is not available via public APIs for these ecosystems.
Description
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).
- 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
}
}
}'
-
The signature check fails (no
X-ANET-Signatureheader), butgetTransactionDetails('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'samount: 99999.99andmetadata.users_id: 2. -
processSinglePayment()credits $99,999.99 to user ID 2's wallet viaaddBalance(). -
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.
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_idin 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;
}
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐘Packagist | wwbn/avideo | all versions | 29.0composer require wwbn/avideo:^29.0 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for wwbn/avideo, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update wwbn/avideo to 29.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-95jh-7r58-xmxw is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-95jh-7r58-xmxw can be triaged on real exposure rather than presence alone.
Tailored to GHSA-95jh-7r58-xmxw. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-95jh-7r58-xmxw in your dependencies?
O3 Security finds GHSA-95jh-7r58-xmxw across Packagist dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.