VYPR
{\n- const raw = req.header('x-proxy-path') || '';\n- return raw.replace(/[^a-zA-Z0-9\\-_\\/\\.]/g, '');\n-};\n+import {sanitizeProxyPath} from '../../utils/sanitizeProxyPath';\n```\n\n`src/node/hooks/express/admin.ts` — sanitise the value AND emit cache-key/cache-control headers so a shared cache can't collapse responses across different proxy-path values:\n\n```diff\n if (ext === \".html\" || ext === \".js\" || ext === \".css\") {\n- if (req.header(PROXY_HEADER)) {\n+ const proxyPath = sanitizeProxyPath(req);\n+ if (proxyPath) {\n let string = data.toString()\n- dataToSend = string.replaceAll(\"/admin\", req.header(PROXY_HEADER) + \"/admin\")\n- dataToSend = dataToSend.replaceAll(\"/socket.io\", req.header(PROXY_HEADER) + \"/socket.io\")\n+ dataToSend = string.replaceAll(\"/admin\", proxyPath + \"/admin\")\n+ dataToSend = dataToSend.replaceAll(\"/socket.io\", proxyPath + \"/socket.io\")\n }\n+ res.setHeader('Vary', 'x-proxy-path');\n+ res.setHeader('Cache-Control', 'private, no-store');\n }\n```\n\n## Resources\n\n- Patched in: https://github.com/ether/etherpad/pull/7784 (squash commit `8c6104c`).\n- Admin XSS vulnerable code introduced in: https://github.com/ether/etherpad/commit/63e9b2d (PR #6399), released in v2.1.0.\n- Open-redirect vulnerable code introduced in: https://github.com/ether/etherpad/commit/451bd9c (PR #7710), released in v3.0.0.\n\n## Credits\n\nReported during an internal security audit by Claude (via @JohnMcLear).","additionalType":"https://schema.org/SoftwareApplication","sameAs":["https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2026-55087"]},"keywords":"CVE-2026-55087, medium, Etherpad Etherpad Lite","mentions":[{"@type":"SoftwareApplication","name":"Etherpad Lite","applicationCategory":"SecurityApplication","publisher":{"@type":"Organization","name":"Etherpad"}}],"isAccessibleForFree":true},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://portal.vyprsec.ai/"},{"@type":"ListItem","position":2,"name":"CVEs","item":"https://portal.vyprsec.ai/cves"},{"@type":"ListItem","position":3,"name":"CVE-2026-55087","item":"https://portal.vyprsec.ai/cves/CVE-2026-55087"}]}]}
Medium severity6.1NVD Advisory· Published Aug 13, 2026· Updated Aug 13, 2026

ep_etherpad-lite: Cache-poisoning Cross-site Scripting and Open Redirect via x-proxy-path Header

CVE-2026-55087

Description

# GHSA-03 — x-proxy-path header reflected into admin HTML/JS/CSS (cache-poisoning XSS) and concatenated into redirect (open-redirect)

Severity: Medium CVSS v3.1 vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N CVSS suggested base score: ~6.1 — Medium *(Re-validate in the first.gov calculator before filing. Score depends heavily on whether you assume a cooperative cache exists in front of the deployment — single-origin admin-only ops with no shared cache push toward 4.x; cache-poisoning against a CDN pushes toward 7.x.)* CWE: CWE-79 Improper Neutralization of Input During Web Page Generation, CWE-601 URL Redirection to Untrusted Site, CWE-444 Inconsistent Interpretation of HTTP Requests

Title

x-proxy-path request header is interpolated into admin HTML/JS/CSS without sanitisation (cache-poisoning XSS) and into a /p/:pad/timeslider redirect target (open-redirect via protocol-relative URL)

Description

Etherpad lets operators run behind a reverse proxy that prefixes every route with a subpath (e.g. /pad/etherpad/...). The proxy is expected to set x-proxy-path: /pad/etherpad on every request so that server-rendered links, asset URLs, and redirects know to include the prefix. Two server-side call sites historically processed this header:

Issue 3a — src/node/hooks/express/admin.ts (XSS, cache-poisoning)

The admin static-serving handler read req.header('x-proxy-path') and substituted it into the response body of every .html/.js/.css asset under /admin/* using String.prototype.replaceAll. The value was used raw, with no character filter and no Vary / Cache-Control headers on the response. Consequently:

  • An attacker who can issue a request with a chosen x-proxy-path value gets that value reflected into HTML/JS/CSS sent back to them. Reflected XSS on the admin origin (requires victim to be tricked into issuing the request from a context that interprets HTML).
  • More seriously, any reverse proxy or CDN in front of Etherpad that caches /admin/index.html keyed on URL alone (the common case — no Vary was set) will cache the poisoned response and serve it to subsequent admins. Cache-poisoning XSS against every admin that loads the same bundle from the same cache.

Issue 3b — src/node/hooks/express/specialpages.ts (open-redirect via protocol-relative URL)

The legacy /p/:pad/timeslider handler (direct visits without ?embed=1) built a redirect target as:

res.redirect(302, `${proxyPath}/p/${encodeURIComponent(req.params.pad)}`);

A local sanitizeProxyPath helper filtered the character class but did NOT prevent values beginning with //. A request carrying x-proxy-path: //evil.example therefore produced a Location: //evil.example/p/ header, which browsers interpret as a protocol-relative URL — equivalent to https://evil.example/p/. Open redirect, exploitable for phishing.

Both issues require the x-proxy-path header to actually reach Etherpad. In a hardened reverse-proxy deployment the proxy strips/overrides client headers, but Etherpad does not enforce this and self-hosted users with misconfigured proxies (or no proxy at all, where any client sets arbitrary headers) are exposed.

Severity rationale

  • AV:N / AC:L / PR:N — the admin path requires no authentication of the attacker. The victim of the XSS must be an authenticated admin who loads a poisoned cached response.
  • UI:R — victim must visit/interact with the admin UI.
  • S:C — scope changes (attacker context to admin origin).
  • C:L / I:L — XSS in the admin context can read/write admin-scoped data; full admin-account takeover requires additional CSRF-style chaining.

CVSS lands at 6.1 (Medium). Operators behind a well-configured proxy that strips client x-proxy-path are not exposed.

Affected versions

  • Admin XSS (Issue 3a): ep_etherpad-lite >= 2.1.0, <= 3.0.0. The unsanitised replaceAll("/admin", req.header(PROXY_HEADER) + ...) was present in `63e9b2d` "Fixed api header authorization" (#6399), first tagged in v2.1.0 (2024-05-22). All releases through v3.0.0 carry it.
  • Open-redirect (Issue 3b): ep_etherpad-lite = 3.0.0. The legacy timeslider redirect that concatenates the proxy path into a Location header was introduced in `451bd9c` "scrub history in-place on the pad URL" (#7710) and first shipped in v3.0.0. Pre-v3 releases serve the timeslider directly without a redirect and are not exposed to this specific shape.
  • Combined fix-target range covered by the GHSA: >= 2.1.0, <= 3.0.0.

Patched versions

  • ep_etherpad-lite >= 3.1.0 — the fix is on develop HEAD as commit 8c6104c. Update this field with the actual tagged release version when it ships.

Proof of concept

XSS / cache poisoning

curl -s 'https://pad.example/admin/index.html' \
  -H 'x-proxy-path: "> {
-  const raw = req.header('x-proxy-path') || '';
-  return raw.replace(/[^a-zA-Z0-9\-_\/\.]/g, '');
-};
+import {sanitizeProxyPath} from '../../utils/sanitizeProxyPath';

src/node/hooks/express/admin.ts — sanitise the value AND emit cache-key/cache-control headers so a shared cache can't collapse responses across different proxy-path values:

   if (ext === ".html" || ext === ".js" || ext === ".css") {
-    if (req.header(PROXY_HEADER)) {
+    const proxyPath = sanitizeProxyPath(req);
+    if (proxyPath) {
       let string = data.toString()
-      dataToSend = string.replaceAll("/admin", req.header(PROXY_HEADER) + "/admin")
-      dataToSend = dataToSend.replaceAll("/socket.io", req.header(PROXY_HEADER) + "/socket.io")
+      dataToSend = string.replaceAll("/admin", proxyPath + "/admin")
+      dataToSend = dataToSend.replaceAll("/socket.io", proxyPath + "/socket.io")
     }
+    res.setHeader('Vary', 'x-proxy-path');
+    res.setHeader('Cache-Control', 'private, no-store');
   }

Resources

  • Patched in: https://github.com/ether/etherpad/pull/7784 (squash commit 8c6104c).
  • Admin XSS vulnerable code introduced in: https://github.com/ether/etherpad/commit/63e9b2d (PR #6399), released in v2.1.0.
  • Open-redirect vulnerable code introduced in: https://github.com/ether/etherpad/commit/451bd9c (PR #7710), released in v3.0.0.

Credits

Reported during an internal security audit by Claude (via @JohnMcLear).

Affected products

1

Patches

Vulnerability mechanics

References

7

News mentions

0

No linked articles in our index yet.