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.
safeString() (objects/functions.php:375-410 at master ) strips tags first and decodes entities last:
Video::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
view/videoEmbeded.php:363 (the 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 body and its title attribute)
No Content-Security-Policy is sent, so the inline handler runs.
The root cause is acc85b7 (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. 44c1225 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.
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 ).
Output (PHP 8.5; the two report cell lines show the reportMyVideos rendering before and after 44c1225 ):
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.
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.
AVideo/objects/functions.php Lines 375 to 410 in 5e1701f function safeString ( $ text , $ strict = false , $ try = 0 ) { if ( empty ( $ text )) { return '' ; } $ originalText = $ text ; $ 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 ' ); if ( $ strict ) { $ text = filter_var ( $ text , FILTER_SANITIZE_FULL_SPECIAL_CHARS ); //$text = cleanURLName($text); } $ text = trim ( $ text ); if ( empty ( $ try ) && empty ( $ text ) && function_exists ( ' mb_convert_encoding ' )) { $ originalText2 = preg_replace ( ' /[^\PC\s]/u ' , '' , $ originalText ); if ( empty ( $ originalText2 )) { $ originalText2 = mb_convert_encoding ( $ originalText , ' UTF-8 ' , ' auto ' ); $ originalText2 = preg_replace ( ' /[^\PC\s]/u ' , '' , $ originalText2 ); } if (! empty ( $ originalText2 )) { $ originalText = $ originalText2 ; } // Remove leading and trailing whitespace $ originalText = trim ( $ originalText ); return safeString ( mb_convert_encoding ( $ originalText , ' UTF-8 ' ), $ strict , 1 ); } return $ text ; }
AVideo/objects/functions.php
Lines 375 to 410 in 5e1701f
AVideo/objects/video.php Line 4170 in 5e1701f $ title = safeString ( $ title );
AVideo/objects/video.php
AVideo/objects/video.php Line 669 in 5e1701f $ this -> title = (( safeString ( $ this -> title )));
AVideo/objects/video.php
AVideo/view/trending.php Line 80 in 5e1701f
AVideo/view/trending.php
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.
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
