CVE-2026-43633
Description
HestiaCP versions 1.9.0 through 1.9.4 contain a deserialization vulnerability in the web terminal component caused by a session format mismatch between PHP and Node.js that allows unauthenticated remote attackers to achieve root-level code execution. Attackers can inject crafted data into HTTP headers that are processed by the PHP session handler but incorrectly deserialized by the Node.js web terminal component as trusted session values, resulting in arbitrary command execution on systems with the web terminal feature enabled.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Unauthenticated remote code execution as root in HestiaCP 1.9.0-1.9.4 via deserialization mismatch between PHP and Node.js session handling in the web terminal.
Vulnerability
HestiaCP versions 1.9.0 through 1.9.4 contain a deserialization vulnerability in the web terminal component. The PHP session handler writes attacker-controlled data into session files using a length-prefixed serialization format, while the Node.js web terminal component reads the same files using naive string splitting that does not respect serialization boundaries [1][4]. This mismatch allows an attacker to inject crafted data into HTTP headers that are processed by PHP and then incorrectly deserialized by Node.js as trusted session values [1]. The web terminal feature must be enabled for exploitation [1].
Exploitation
An unauthenticated attacker can send specially crafted HTTP requests to the HestiaCP web interface on port 8083. By injecting malicious data into the Cookie header, the attacker can cause the Node.js web terminal to interpret a forged session as valid [1]. The attacker then establishes a WebSocket connection to the web terminal, which executes arbitrary commands with root privileges [1][4]. The attack requires no authentication and can be performed remotely over the network [1]. The advisory notes that the exploit can be completed in two HTTP requests [1].
Impact
Successful exploitation allows an unauthenticated remote attacker to execute arbitrary commands as root on the affected HestiaCP server [1][4]. This results in full compromise of the system, including complete loss of confidentiality, integrity, and availability. The attacker can install backdoors, exfiltrate data, or pivot to other systems [1]. The vulnerability is rated CVSS 10.0 (Critical) [4].
Mitigation
As of the publication date (19 May 2026), a fix has been merged into the main branch of HestiaCP (commit 854d71b) but no official release has been published [1][2]. Users are advised to immediately disable the web terminal feature using the command v-delete-sys-web-terminal and restrict access to port 8083 to trusted IPs via firewall rules [1]. The vulnerability is not listed on CISA's Known Exploited Vulnerabilities (KEV) catalog as of the advisory date [1]. Users should monitor for a patched release and apply it promptly [1].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
1854d71b3c173web-terminal: use php helper for session auth lookup (#5244)
3 files changed · +119 −10
src/deb/web-terminal/server.js+65 −10 modified@@ -1,6 +1,6 @@ #!/usr/bin/env node -import { execSync } from 'node:child_process'; +import { execFileSync, execSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; import { spawn } from 'node-pty'; import { WebSocketServer } from 'ws'; @@ -14,10 +14,48 @@ const { config } = JSON.parse( execSync(`${process.env.HESTIA}/bin/v-list-sys-config json`, { silent: true }).toString(), ); +function parseCookies(cookieHeader) { + const cookies = {}; + if (typeof cookieHeader !== 'string' || cookieHeader.length === 0) { + return cookies; + } + + for (const part of cookieHeader.split(';')) { + const cookie = part.trim(); + if (cookie.length === 0) { + continue; + } + + const separatorIndex = cookie.indexOf('='); + if (separatorIndex < 0) { + cookies[cookie] = cookies[cookie] || []; + cookies[cookie].push(''); + continue; + } + + if (separatorIndex === 0) { + continue; + } + + const key = cookie.slice(0, separatorIndex).trim(); + const value = cookie.slice(separatorIndex + 1).trim(); + if (key.length === 0) { + continue; + } + + cookies[key] = cookies[key] || []; + cookies[key].push(value); + } + + return cookies; +} + const wss = new WebSocketServer({ port: Number.parseInt(config.WEB_TERMINAL_PORT, 10), verifyClient: async (info, cb) => { - if (!info.req.headers.cookie.includes(sessionName)) { + const cookies = parseCookies(info.req.headers.cookie); + const sessionIDs = cookies[sessionName] || []; + if (sessionIDs.length !== 1 || sessionIDs[0].length === 0) { cb(false, 401, 'Unauthorized'); return; } @@ -48,20 +86,37 @@ wss.on('connection', (ws, req) => { const remoteIP = req.headers['x-real-ip'] || req.socket.remoteAddress; // Check if session is valid - const sessionID = req.headers.cookie.split(`${sessionName}=`)[1].split(';')[0]; + const cookies = parseCookies(req.headers.cookie); + const sessionIDs = cookies[sessionName] || []; + if (sessionIDs.length !== 1 || sessionIDs[0].length === 0) { + console.error(`Missing ${sessionName} cookie from ${remoteIP}, refusing connection`); + ws.close(1000, 'You are not authenticated.'); + return; + } + const sessionID = sessionIDs[0]; console.log(`New connection from ${remoteIP} (${sessionID})`); - const file = readFileSync(`${process.env.HESTIA}/data/sessions/sess_${sessionID}`); - if (!file) { - console.error(`Invalid session ID ${sessionID}, refusing connection`); + let authResult; + try { + const raw = execFileSync( + `${process.env.HESTIA}/php/bin/php`, + [`${process.env.HESTIA}/web-terminal/web-terminal-session-auth.php`, sessionID], + { encoding: 'utf8' }, + ); + authResult = JSON.parse(raw); + } catch (error) { + console.error(`Session helper failed for ${sessionID}, refusing connection: ${error.message}`); ws.close(1000, 'Your session has expired.'); return; } - const session = file.toString(); - // Get username - const login = session.split('user|s:')[1].split('"')[1]; - const impersonating = session.split('look|s:')[1].split('"')[1]; + if (!authResult?.ok || typeof authResult.user !== 'string' || authResult.user.length === 0) { + console.error(`Unauthenticated session ${sessionID}, refusing connection`); + ws.close(1000, 'You are not authenticated.'); + return; + } + const login = authResult.user; + const impersonating = typeof authResult.look === 'string' ? authResult.look : ''; const username = impersonating.length > 0 ? impersonating : login; // Get user info
src/deb/web-terminal/web-terminal-session-auth.php+52 −0 added@@ -0,0 +1,52 @@ +#!/usr/local/hestia/php/bin/php +<?php +declare(strict_types=1); + +function deny(string $error, int $code = 1): never { + echo json_encode( + ["ok" => false, "error" => $error], + JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR, +), + PHP_EOL; + exit($code); +} + +if (!isset($argv[1]) || !is_string($argv[1])) { + deny("missing session id"); +} + +$sessionId = $argv[1]; +if ($sessionId === "" || preg_match('/^[A-Za-z0-9,-]+$/', $sessionId) !== 1) { + deny("invalid session id"); +} + +$hestia = getenv("HESTIA"); +if (!is_string($hestia) || $hestia === "") { + deny("missing HESTIA env"); +} + +session_name("HESTIASID"); +session_save_path($hestia . "/data/sessions"); +session_id($sessionId); + +if (!@session_start()) { + deny("session start failed"); +} + +$user = $_SESSION["user"] ?? ""; +$look = $_SESSION["look"] ?? ""; + +if (!is_string($user) || $user === "") { + deny("unauthenticated"); +} + +if (!is_string($look)) { + $look = ""; +} + +echo json_encode( + ["ok" => true, "user" => $user, "look" => $look], + JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR, +), + PHP_EOL; +
src/hst_autocompile.sh+2 −0 modified@@ -603,7 +603,9 @@ if [ "$WEB_TERMINAL_B" = true ]; then get_branch_file 'src/deb/web-terminal/package.json' "${BUILD_DIR_HESTIA_TERMINAL}/usr/local/hestia/web-terminal/package.json" get_branch_file 'src/deb/web-terminal/package-lock.json' "${BUILD_DIR_HESTIA_TERMINAL}/usr/local/hestia/web-terminal/package-lock.json" get_branch_file 'src/deb/web-terminal/server.js' "${BUILD_DIR_HESTIA_TERMINAL}/usr/local/hestia/web-terminal/server.js" + get_branch_file 'src/deb/web-terminal/web-terminal-session-auth.php' "${BUILD_DIR_HESTIA_TERMINAL}/usr/local/hestia/web-terminal/web-terminal-session-auth.php" chmod +x "${BUILD_DIR_HESTIA_TERMINAL}/usr/local/hestia/web-terminal/server.js" + chmod +x "${BUILD_DIR_HESTIA_TERMINAL}/usr/local/hestia/web-terminal/web-terminal-session-auth.php" cd $BUILD_DIR_HESTIA_TERMINAL/usr/local/hestia/web-terminal npm ci --omit=dev
Vulnerability mechanics
Root cause
"The Node.js web-terminal server directly parsed PHP session files using string splitting, treating attacker-controlled cookie values as trusted session data without cryptographic verification."
Attack vector
An unauthenticated attacker sends a WebSocket upgrade request to the web-terminal server with a crafted `Cookie` header containing a `HESTIASID` value that matches a PHP session file path. The old Node.js code [patch_id=623412] naively split the cookie header and read the corresponding session file from disk, then parsed the file contents with simple string operations (e.g., `.split('user|s:')[1].split('"')[1]`). Because PHP serializes session data in a predictable format and the Node.js code did not validate the session's cryptographic integrity, an attacker who can write arbitrary content into a session file (e.g., via another unauthenticated endpoint or a race condition) can inject a crafted `user` value. The attacker-supplied `user` value is then used to spawn a pty session with root privileges, achieving unauthenticated remote code execution as root.
Affected code
The vulnerable code resides in `src/deb/web-terminal/server.js` [patch_id=623412]. The `verifyClient` callback and the `connection` event handler both parsed the `Cookie` header with a naive `info.req.headers.cookie.includes(sessionName)` check and `req.headers.cookie.split(...)` to extract the session ID. The session file was then read directly with `readFileSync` and parsed via string splitting (e.g., `session.split('user|s:')[1].split('"')[1]`), trusting the file contents without any cryptographic or PHP-native deserialization.
What the fix does
The patch [patch_id=623412] replaces the insecure server-side session file parsing with a dedicated PHP helper script (`web-terminal-session-auth.php`). This PHP script uses PHP's native `session_start()` and `$_SESSION` superglobal to properly deserialize the session data, then outputs a JSON response containing only the authenticated `user` and `look` values. The Node.js server now calls this helper via `execFileSync` and validates the JSON response for an `"ok": true` flag and a non-empty `user` string before granting access. Additionally, the cookie parsing was hardened with a proper `parseCookies` function that correctly handles multiple cookies and edge cases, preventing header injection tricks that could bypass the old `includes()` check.
Preconditions
- configThe web terminal feature must be enabled in HestiaCP configuration.
- networkAttacker must have network access to the web-terminal WebSocket port (default configurable via WEB_TERMINAL_PORT).
- inputAttacker must be able to inject or control content in a PHP session file under /usr/local/hestia/data/sessions/ (e.g., via another unauthenticated endpoint or session fixation).
Generated on May 19, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/hestiacp/hestiacp/commit/854d71b3c1737b0a0d0cc55c926008ffe1f6719bnvd
- github.com/hestiacp/hestiacp/issues/5229nvd
- github.com/hestiacp/hestiacp/pull/5244nvd
- mercuryiss.com.au/hestiacp-unauthenticated-rce-ip-spoofing-cve-2026-43633-cve-2026-43634nvd
- www.vulncheck.com/advisories/hestiacp-deserialization-rce-via-web-terminalnvd
News mentions
0No linked articles in our index yet.