github ether/etherpad v3.3.0

4 hours ago

3.3.0

3.3 is primarily a security-hardening release. A defence-in-depth pass tightens the HTTP API entry points, switches random-id generation to a CSPRNG, escapes exported data-* attributes, and flips the shipped Docker deployment defaults so a fresh install no longer boots with implicit credentials or a trusting proxy. Alongside that, the ep_* pad-options passthrough that shipped opt-in in 3.0.0 is now on by default, the in-pad timeslider learns to honour the editor's view settings (authorship colours, font family, line numbers), and a long tail of pad-editor layout, RTL, and URL-encoding fixes lands. The release also carries the root-cause fix for the long-standing Windows backend-test "silent ELIFECYCLE" flake.

Notable enhancements

  • Plugin pad options on by default — settings.enablePluginPadOptions now defaults to true (#7841). The flag that gates the ep_* passthrough on pad options (shipped opt-in in 3.0.0, #7698) is flipped to default-on, so plugins such as ep_plugin_helpers' padToggle / padSelect ride the existing broadcast/persist rail out of the box. This closes ep_comments_page#422 — stock 3.x deployments console.warned on every pad load because the helper detected enablePluginPadOptions === false. The settings.json.template env-var default is flipped to match, so Docker/supervisor configs without an explicit value get the new behaviour. Existing deployments with an explicit "enablePluginPadOptions": false keep that value — no migration needed — and the protocol shape is unchanged for older clients.
  • Timeslider — honour the editor's view settings (#7899). The in-pad timeslider now respects showAuthorshipColors, padFontFamily, and line-numbers, bridged from the pad-settings checkboxes into the embedded timeslider iframe so the two views agree. nice-select.ts dispatches a native change event after the jQuery trigger so the addEventListener-based bridge in pad_mode.ts fires (jQuery 3.7.1's trigger() does not dispatch native DOM events), and the font-family reset is fixed for jQuery 3 (which ignores a null css value). The five ad-hoc listener stores in pad_mode.ts are consolidated into one bindOuter() path and the three view-setting bridges into a single data-driven bridgeView() (refactor only).
  • Admin settings — explain env-var substitution and surface auth errors (#7819 / #7826). Three env-var-only UX improvements driven by #7819 (a Docker operator saved an ep_oauth block in the Raw view and reported it "disappeared", not realising settings.json on disk is a template, not the effective config): a banner above the editor explaining the template/substitution model (rendered only when the loaded file contains a ${VAR} placeholder); a read-only Effective tab exposing the redacted runtime settings the backend already emitted as resolved (also gated on ${VAR}); and an admin_auth_error event so a misrouted Traefik+SSO session that isn't admin gets a clear toast instead of a silent "save did nothing". A reconnect-loop guard suppresses the SPA's auto-reconnect once an auth error has been received. No behaviour change for installs without ${VAR} placeholders.

Security hardening

A defence-in-depth pass across the API, token, export, and deployment surfaces:

  • HTTP API request handling, random IDs, and plugin loading (#7906). pad_utils.randomString now generates random IDs via crypto.getRandomValues (CSPRNG) instead of Math.random. OAuth2Provider compares passwords with crypto.timingSafeEqual on the raw UTF-8 bytes (resolving the CodeQL "insufficient computational effort" alert) behind a uniform failure delay, and looks users up via own-property access only. API.appendChatMessage throws padID does not exist rather than creating the pad, consistent with the other content API methods. The /api/2 REST router forwards only the authorization header (not the full request header set) and falls back to it whenever the field is falsy, matching the openapi.ts handler so both routers authenticate identically. LinkInstaller validates plugin dependency names before building filesystem paths from them, and the admin file server returns a generic error while logging details server-side.
  • Escape exported data-* attributes; warn on default/placeholder credentials (#7905). ExportHtml now escapes the name and value of attributes emitted by the exportHtmlAdditionalTagsWithData hook, consistent with the URL/text escaping already applied to exported HTML. Settings logs a warning (error level under NODE_ENV=production) when an account uses a default/placeholder password from the shipped config, and the check is extended to cover sso.clients[].client_secret so enabling SSO without setting ADMIN_SECRET / USER_SECRET is flagged the same way.
  • Docker deployment defaults — require explicit credentials, default TRUST_PROXY off (#7907). The shipped docker-compose now requires ADMIN_PASSWORD and the database password to be provided explicitly (no implicit fallback) and defaults TRUST_PROXY to false. Operators relying on the previous implicit defaults must now set these values explicitly.

Notable fixes

  • History mode — lay the timeslider iframe in the editor's flex slot (#7903). In-pad history mode positioned #history-frame-mount as an inset:0 absolute overlay over #editorcontainerbox, which took the iframe out of flow and hid any in-flow side panel (e.g. ep_webrtc's #rtcbox video column) beneath it — so history mode and live mode disagreed. The iframe now occupies the same in-flow flex slot the live editor uses, and a latent specificity bug (the body.history-mode #editorcontainer { display: none } hide rule was outranked by the two-id layout rule, so the live editor was only ever painted over) is fixed by giving the hide rule matching specificity. Adds a padmode.spec.ts regression test.
  • Pad editor — restore URL wrapping (#7894 / #7896). Long URLs in the pad editor overflowed instead of wrapping because the global a { white-space: nowrap } rule overrode the wrapping properties on #innerdocbody. Explicit white-space / word-wrap / overflow-wrap on #innerdocbody a restores wrapping inside the editor while preserving no-wrap for links elsewhere in the UI.
  • RTL content option no longer flips the whole page (#7900 / #7901). The per-pad RTL content option (rtlIsTrue) wrote the direction to the top-level document.documentElement, flipping the entire page — toolbar and chrome included. The content direction is now applied to the inner editor document (targetDoc.documentElement); page direction stays owned by the UI language (l10n.ts). Adds a frontend test asserting the inner editor flips while the top-level <html> dir is unchanged.
  • Pad-wide view settings apply to the creator's own view (#7900 / #7902). Because a creator is never "enforced upon themselves", a stale personal view-override cookie (e.g. rtlIsTrue=false from an earlier toggle) silently masked the pad-wide value they later set, so the control appeared to do nothing on their own screen. Changing a pad-wide view option now syncs the creator's personal pref to the chosen value; the precedence model is unchanged (the creator can still override afterwards via "My view").
  • URL view-option params lost to a padeditor.init race (#7840 / #7843). ?showLineNumbers=false and ?useMonospaceFont=true were silently clobbered shortly after load — the same race #7464 fixed for ?rtl=false, but the neighbouring showLineNumbers / noColors / useMonospaceFontGlobal blocks were left at the synchronous-tail site. The fix is generalised to all three (moved into postAceInit). Mostly observable in cross-context iframe embeds that start with no prefs cookie. Adds url_view_options.spec.ts.
  • Default welcome text attributed to the system author (#7885 / #7887). Auto-generated default pad content (settings.defaultPadText / padDefaultContent hook) carried the creating user's author attribute and rendered in their authorship colour, even though they never wrote it. The welcome text's author attribute is now Pad.SYSTEM_AUTHOR_ID, while revision 0's meta.author stays the real creator so ownership (pad-wide settings gate, deletion token) is preserved. Explicitly provided text (e.g. HTTP API createPad with text + author) keeps the real author.
  • URL-encode pad names in the admin 'Open' button and recent pads (#7865 / #7895). Pad names are encodeURIComponent-d in the admin PadPage Open href and the colibris recent-pads href, and decodeURIComponent-d when read back from the URL pathname; legacy URL-encoded recent-pads names are normalised before re-encoding to prevent double-encoding (%2F%252F). The admin Open window.open gains noopener,noreferrer.
  • OIDC — fix broken OIDCAdapter flows (#7837). Repairs the adapter flows and widens the storage type to include string for the userCode index; adds regression tests.
  • Accessibility — dialog titles/descriptions and a missing l10n key (#7835 / #7836). Adds the index.code key referenced by index.html but never defined (which produced a "Couldn't find translation key" console error on the landing page), and gives every admin @radix-ui/react-dialog Dialog.Content a Dialog.Title and Dialog.Description (visually hidden where there's no visible heading), silencing Radix's a11y warnings. A new backend spec fails CI if any data-l10n-id in src/templates/*.html is missing from en.json.
  • Offline/air-gapped Docker boot — stop pnpm self-provisioning a pinned version (issue #7911). The official image installs pnpm directly (corepack was dropped for Node 25+). Because the image's pnpm intentionally lags the packageManager pin in package.json (pnpm 11.1.x enforces a minimum-release-age policy the frozen-lockfile build can't satisfy), pnpm treated every call — including the informational pnpm --version probe Etherpad runs at startup — as a request to download the pinned build. Behind a firewall that download failed (Failed to get pnpm version: … Command exited with code 1), breaking startup. The Dockerfile now sets pnpm_config_pm_on_fail=ignore, and the startup probe plus the updater's pnpm-on-PATH checks run with the same flag, so pnpm uses the installed version instead of reaching for the network (without changing which pnpm runs the build-time install). A backend spec fails CI if that guard is dropped while a version gap exists.
  • Firefox authorship colours — tag early keystrokes with the right author (#7910). The inner editor's thisAuthor starts empty and is only populated when collab_client's queued setProperty('userAuthor', userId) reaches the iframe (applied asynchronously via pendingInit). Under Firefox timing the first keystrokes could beat it, so freshly typed text — and early line-attribute changes (lists, headings, alignment) — were tagged author='', which canonicalises to an unattributed insert that the server's pad-corruption guard rejects, dropping the whole change and losing authorship (the intermittent clear_authorship_color flake, where undo couldn't restore the author colour). A getLocalAuthor() helper now falls back to clientVars.userId (the same id, available synchronously) whenever thisAuthor is still empty, applied at the text-insert sites and to seed documentAttributeManager.author; the intentional clear-authorship path and the server-side guard are unchanged.
  • Dark mode — fix the white address bar and the light-flash on load (#7909, issue #7606). Dark-mode users still saw a white mobile address bar above the dark toolbar, and the whole page flashed light before going dark. Both came from rendering the light state server-side and switching to dark only after the JS bundle ran: iOS Safari reads theme-color at parse time and doesn't reliably repaint on a later JS mutation, and the page painted light before the bundle applied the dark skin classes. The server now emits a prefers-color-scheme-scoped theme-color pair so the address bar is correct at first paint, plus a small blocking <head> script that applies the dark skin classes before the stylesheet paints. Both are gated on enableDarkMode (default on) and the colibris skin; pad.ts still runs on init to wire up the #options-darkmode toggle (which now updates every theme-color meta) and theme the editor iframes. Applies to the pad and timeslider views.

Internal / contributor-facing

  • Root-caused and fixed the Windows backend-test "silent ELIFECYCLE" flake (#7866). The ~22% Windows flake — rotating across random spec files, no mocha summary, no JS trace — was diagnosed from a full-memory dump as two distinct causes. (1) A timing-fragile test abandoned by mocha keeps running and later throws an orphan unhandled rejection; server.ts's process-global uncaughtException/unhandledRejection handlers (correct for a real Etherpad process) escalated that into a clean process.exit. They are now gated behind require.main === module, and the backend-test bootstraps (common.ts, diagnostics.ts) log orphan rejections instead of rethrowing. (2) A stack-buffer overrun in Node 24.x's bundled libuv Windows TCP-connect path (uv__tcp_connect) corrupts memory under the suite's localhost-connection churn; CI pins the Windows backend job to Node 24.16.0 (libuv 1.52.1, the bisected fix), referencing upstream nodejs/node#63620. Linux stays on Node 24 LTS.
  • Removed the now-unneeded ELIFECYCLE diagnostic scaffolding (#7846 / #7838 / #7842 / #7868). The OS-level sidecar watcher, the diagnostics heartbeat/running-test pointer, and the mid-test snapshot — added to chase the flake above — are removed now that the cause is known.
  • Docs — document the Docker settings.json writable-layer and env-var-vs-file semantics (#7819 / #7827). Two operator-facing gaps surfaced by #7819: that the on-disk settings.json is a template (env substitution happens in memory at load time), and that the default compose puts settings.json in the container's writable layer with no host mount, so admin edits are lost on down/pull/watchtower but survive a plain restart. Adds prose + a recreate-vs-restart table to doc/docker.md and a commented-out opt-in bind mount to the compose files.
  • Docs refresh for 3.2.0 (#7888), dropped three redundant top-level files (#7839), dropped a fragile viewport assertion in the enter test (#7845), and a backend-test fix-up.

Dependencies

  • Two major bumps: redis 5.12.1 → 6.0.0 (#7869) and ejs 5.0.2 → 6.0.1 (#7860).
  • ueberdb2 6.1.2 → 6.1.8, mssql 12.5.3 → 12.5.5, nodemailer 8.0.7 → 8.0.10, mysql2 3.22.3 → 3.22.5 (#7915), undici 8.3.0 → 8.4.1 (#7914), pdfkit 0.18.0 → 0.19.0 (#7916), oidc-provider 9.8.3 → 9.8.4, @elastic/elasticsearch 9.4.1 → 9.4.2, lru-cache 11.5.0 → 11.5.1, rate-limiter-flexible 11.1.0 → 11.1.1, semver 7.8.1 → 7.8.2, js-cookie 3.0.7 → 3.0.8, tsx 4.22.3 → 4.22.4, @radix-ui/react-switch 1.2.6 → 1.3.0 (#7913), @tanstack/react-query 5.100.11 → 5.101.0 (+ devtools), plus i18next, react-router-dom, and several dev-dependency group bumps (#7912).

Localisation

  • Multiple updates from translatewiki.net.

Don't miss a new etherpad release

NewReleases is sending notifications on new releases.