mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-21 00:59:11 +00:00
* docs: design for URL base-path support (#7802) Spec covers the architecture, header handling rules, components touched, backwards-compatibility story, risks, and test plan for honoring X-Forwarded-Prefix / X-Ingress-Path under trustProxy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: amend #7802 spec with discovery of pre-existing proxy-path helpers After exploring the codebase, much of the proposed architecture is already in place (sanitizeProxyPath, padBootstrap.js basePath derivation, admin SPA rewrite). Spec now reflects the actual delta: header source expansion, /manifest.json prefix-awareness, socialMeta proxyPath honoring, and template URL touch-ups for index/timeslider/pad/export. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(plan): implementation plan for URL base-path support (#7802) Adds the bite-sized TDD task list to ship X-Forwarded-Prefix / X-Ingress-Path support: extends sanitizeProxyPath, makes /manifest.json and socialMeta prefix-aware, touches up the remaining leading-slash URLs in index/pad/timeslider/export templates, fixes a pre-existing manifest .. count bug. Drops the originally-proposed <base href> belt-and-braces after discovering it'd break the existing relative URLs in pad.html/timeslider.html and wouldn't help plugin DOM injection anyway (path-absolute URLs ignore <base>'s path component). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(proxy): accept X-Forwarded-Prefix and X-Ingress-Path under trustProxy (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(pwa): make /manifest.json honor sanitised proxy-path (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(social-meta): honor proxyPath in from-request og:url and og:image (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(templates): index.html manifest + jslicense links honor proxyPath (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(templates): pad.html reconnect/jslicense honor proxyPath; fix manifest .. count (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(templates): timeslider.html reconnect/jslicense honor proxyPath; fix manifest .. count (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(templates): export_html.html manifest honors proxyPath when available (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: end-to-end coverage for X-Forwarded-Prefix / X-Ingress-Path (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(settings): trustProxy also enables X-Forwarded-Prefix / X-Ingress-Path (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4d998d6ef2
commit
86edd67f58
17 changed files with 2055 additions and 61 deletions
|
|
@ -1,48 +1,65 @@
|
|||
import settings from './Settings';
|
||||
|
||||
/**
|
||||
* Sanitize the `x-proxy-path` request header.
|
||||
* Sanitize the URL-path prefix Etherpad is being served under.
|
||||
*
|
||||
* Etherpad lets operators run behind a reverse proxy that prefixes every
|
||||
* route under a subpath (e.g. `/pad/etherpad/...`). The proxy is expected
|
||||
* to set `x-proxy-path` so that server-rendered links and redirects know
|
||||
* about the prefix. The header value is then woven into HTML, JS, CSS,
|
||||
* and HTTP Location headers — so it must be treated as untrusted input
|
||||
* even if the deployment intends to set it from a trusted proxy.
|
||||
* Headers checked in order; first non-empty (after sanitization) wins:
|
||||
* 1. `x-proxy-path` — Etherpad's own convention; always honored because
|
||||
* the operator must explicitly configure their proxy to send it.
|
||||
* 2. `x-forwarded-prefix` — HAProxy / Traefik standard.
|
||||
* 3. `x-ingress-path` — Home Assistant supervisor ingress.
|
||||
*
|
||||
* Semantics:
|
||||
* - Returns an empty string when the header is absent or unparseable.
|
||||
* The two standard headers (everything other than x-proxy-path) are honored
|
||||
* ONLY when `settings.trustProxy === true`, because they can otherwise be
|
||||
* forged by any internet client when Etherpad runs on a public IP.
|
||||
*
|
||||
* The header value is woven into HTML, JS, CSS and HTTP Location headers,
|
||||
* so the same value is also treated as untrusted input even when read from
|
||||
* a trusted header. Sanitization rules:
|
||||
* - Strips every character outside `[a-zA-Z0-9\-_\/\.]`.
|
||||
* - Collapses a leading `//+` to a single `/` so the value can never
|
||||
* be interpreted as a protocol-relative URL.
|
||||
* - Prepends `/` if the (non-empty) result doesn't already start
|
||||
* with one, so callers can always concatenate the value as an
|
||||
* absolute path prefix.
|
||||
* - Collapses a leading `//+` to a single `/` so the value can never be
|
||||
* interpreted as a protocol-relative URL.
|
||||
* - Prepends `/` if the (non-empty) result doesn't already start with one,
|
||||
* so callers can always concatenate the value as an absolute path prefix.
|
||||
* - Rejects values containing `..` segments.
|
||||
*
|
||||
* The output is always either the empty string or a string that starts
|
||||
* with exactly one `/` and contains only `[A-Za-z0-9\-_./]`.
|
||||
*/
|
||||
export const sanitizeProxyPath = (req: {header: (n: string) => string|undefined} | string | undefined): string => {
|
||||
const raw = typeof req === 'string'
|
||||
? req
|
||||
: req && typeof req.header === 'function'
|
||||
? (req.header('x-proxy-path') || '')
|
||||
: '';
|
||||
|
||||
const HEADER_NAMES = [
|
||||
// [headerName, requiresTrustProxy]
|
||||
['x-proxy-path', false] as const,
|
||||
['x-forwarded-prefix', true] as const,
|
||||
['x-ingress-path', true] as const,
|
||||
];
|
||||
|
||||
const cleanOne = (raw: string): string => {
|
||||
let cleaned = raw.replace(/[^a-zA-Z0-9\-_\/\.]/g, '');
|
||||
if (!cleaned) return '';
|
||||
// Collapse leading "//+" to a single "/" so the value can never be
|
||||
// interpreted as a protocol-relative URL when concatenated into an
|
||||
// href / Location / iframe src.
|
||||
cleaned = cleaned.replace(/^\/{2,}/, '/');
|
||||
// Ensure the value starts with exactly one "/". Several callers
|
||||
// concatenate this as a URL-path prefix (e.g. `${proxyPath}/p/...`
|
||||
// for redirects, `${proxyPath}/watch/...` for entrypoint URLs) and
|
||||
// assume the value is either empty or absolute. A header value like
|
||||
// `pad/etherpad` would otherwise become a relative redirect /
|
||||
// entrypoint and break the page.
|
||||
if (cleaned[0] !== '/') cleaned = '/' + cleaned;
|
||||
// Refuse "/.." / "../" segments — path-traversal shapes that some
|
||||
// downstream URL joiners would still honour even after the character
|
||||
// filter above.
|
||||
if (/(?:^|\/)\.\.(?:\/|$)/.test(cleaned)) return '';
|
||||
return cleaned;
|
||||
};
|
||||
|
||||
type ReqLike = {header: (n: string) => string|undefined};
|
||||
|
||||
export const sanitizeProxyPath = (
|
||||
req: ReqLike | string | undefined,
|
||||
opts: {trustProxy?: boolean} = {},
|
||||
): string => {
|
||||
// String form preserves the original behaviour for callers that pre-extracted
|
||||
// the value themselves (e.g. tests). It's treated as a raw value with no
|
||||
// header-gating: the caller has already decided to use it.
|
||||
if (typeof req === 'string') return cleanOne(req);
|
||||
if (!req || typeof req.header !== 'function') return '';
|
||||
const trustProxy = opts.trustProxy ?? !!settings.trustProxy;
|
||||
for (const [name, requiresTrust] of HEADER_NAMES) {
|
||||
if (requiresTrust && !trustProxy) continue;
|
||||
const raw = req.header(name) || '';
|
||||
const cleaned = cleanOne(raw);
|
||||
if (cleaned) return cleaned;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue