-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Releases: ether/etherpad
v3.3.1
3.3.1
3.3.1 is a small bug-fix and hardening follow-up to 3.3.0. It closes a stored-XSS vector in the numbered-list start attribute, hardens the database layer so a dropped connection to PostgreSQL / Redis / RethinkDB no longer crashes the process (via ueberdb2 6.1.9), and fixes a handful of pad and admin regressions — the iOS dark-mode status bar, the settings language dropdown, the pad-deletion modal under allowPadDeletionByAllUsers, and a single unreadable pad blanking the admin Manage-pads list.
Security
- Pad editor — escape and integer-coerce the numbered-list
startattribute (GHSA-f7h5-v9hm-548j, #7937). A crafted<ol start>value flowed unescaped intodomline.ts, a distinct client-side sink from the export-path fix in 3.3.0's #7905. The value is now integer-coerced and HTML-escaped before it reaches the DOM. A jsdom regression test covers the sink.
Notable fixes
- Skin — paint the root canvas so iOS dark mode has no white status bar (#7606 / #7931). iOS Safari paints the top safe area from the
htmlroot background, whichtheme-color(an Android address-bar hint) does not affect, so dark-mode pads showed a white status-bar strip on iOS. Colibris now sets the root background andcolor-schemeso the safe area matches the editor. - Settings — show the detected language in the dropdown (#7925 / #7928). The settings language
<select>did not reflect the language Etherpad had actually auto-detected; it now shows the active selection. - Pad — don't issue a deletion token (or show its modal) when
allowPadDeletionByAllUsersis on (#7929). With pad deletion open to all users the client still minted a deletion token and surfaced the confirm modal; both are now suppressed in that configuration. - Admin — one unreadable pad no longer empties the Manage-pads list (#7935 / #7938). A single pad that failed to read could throw out of the list-hydration path and blank the entire admin Manage-pads view; the read is now guarded per-pad so the rest of the list still renders.
Internal / contributor-facing
- CI — downstream client compatibility gate (#7923 / #7924 / #7927). A new gate smoke-tests the published
etherpad-pad,etherpad-cli, andetherpad-desktopclients against the server build (Phase 1 + Phase 2), with robust per-client error handling inrun-clients.shso one client's failure is reported rather than masking the others. - CI — verify Etherpad boots offline (#7936). Adds a test step that confirms a built Etherpad starts with no network access.
Dependencies
ueberdb26.1.8 → 6.1.9 — PostgreSQL pool errors are now handled and TCP keep-alive is enabled (fixes #7878), and the Redis and RethinkDB drivers attach connection-error handlers so a dropped database connection no longer crashes the Etherpad process.semver7.8.2 → 7.8.3 (#7933),rate-limiter-flexible11.1.1 → 11.2.0 (#7934), plus a dev-dependencies group update (#7932).
Assets 6
v3.3.0
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.enablePluginPadOptionsnow defaults totrue(#7841). The flag that gates theep_*passthrough on pad options (shipped opt-in in 3.0.0, #7698) is flipped to default-on, so plugins such asep_plugin_helpers'padToggle/padSelectride the existing broadcast/persist rail out of the box. This closesep_comments_page#422— stock 3.x deploymentsconsole.warned on every pad load because the helper detectedenablePluginPadOptions === false. Thesettings.json.templateenv-var default is flipped to match, so Docker/supervisor configs without an explicit value get the new behaviour. Existing deployments with an explicit"enablePluginPadOptions": falsekeep 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.tsdispatches a nativechangeevent after the jQuery trigger so theaddEventListener-based bridge inpad_mode.tsfires (jQuery 3.7.1'strigger()does not dispatch native DOM events), and the font-family reset is fixed for jQuery 3 (which ignores anullcss value). The five ad-hoc listener stores inpad_mode.tsare consolidated into onebindOuter()path and the three view-setting bridges into a single data-drivenbridgeView()(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_oauthblock in the Raw view and reported it "disappeared", not realisingsettings.jsonon 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 asresolved(also gated on${VAR}); and anadmin_auth_errorevent 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.randomStringnow generates random IDs viacrypto.getRandomValues(CSPRNG) instead ofMath.random.OAuth2Providercompares passwords withcrypto.timingSafeEqualon 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.appendChatMessagethrowspadID does not existrather than creating the pad, consistent with the other content API methods. The/api/2REST router forwards only theauthorizationheader (not the full request header set) and falls back to it whenever the field is falsy, matching theopenapi.tshandler so both routers authenticate identically.LinkInstallervalidates 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).ExportHtmlnow escapes the name and value of attributes emitted by theexportHtmlAdditionalTagsWithDatahook, consistent with the URL/text escaping already applied to exported HTML.Settingslogs a warning (error level underNODE_ENV=production) when an account uses a default/placeholder password from the shipped config, and the check is extended to coversso.clients[].client_secretso enabling SSO without settingADMIN_SECRET/USER_SECRETis flagged the same way. - Docker deployment defaults — require explicit credentials, default
TRUST_PROXYoff (#7907). The shippeddocker-composenow requiresADMIN_PASSWORDand the database password to be provided explicitly (no implicit fallback) and defaultsTRUST_PROXYtofalse. 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-mountas aninset:0absolute overlay over#editorcontainerbox, which took the iframe out of flow and hid any in-flow side panel (e.g.ep_webrtc's#rtcboxvideo 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 (thebody.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 apadmode.spec.tsregression 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. Explicitwhite-space/word-wrap/overflow-wrapon#innerdocbody arestores 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-leveldocument.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=falsefrom 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.initrace (#7840 / #7843).?showLineNumbers=falseand?useMonospaceFont=truewere silently clobbered shortly after load — the same race #7464 fixed for?rtl=false, but the neighbouringshowLineNumbers/noColors/useMonospaceFontGlobalblocks were left at the synchronous-tail site. The fix is generalised to all three (moved intopostAceInit). Mostly observable in cross-context iframe embeds that start with noprefscookie. Addsurl_view_options.spec.ts. - Default welcome text attributed to the system author (#7885 / #7887). Auto-generated default pad content (
settings.defaultPadText/padDefaultContenthook) carried the creating user'sauthorattribute and rendered in their authorship colour, even though they never wrote it. The welcome text'sauthorattribute is nowPad.SYSTEM_AUTHOR_ID, while revision 0'smeta.authorstays the real creator so ownership (pad-wide settings gate, deletion token) is preserved. Explicitly provided text (e.g. HTTP APIcreatePadwith 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 adminPadPageOpen href and the colibris recent-pads href, anddecodeURIComponent-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 Openwindow.opengainsnoopener,noreferrer. - OIDC — fix broken
OIDCAdapterflows (#7837). Repairs the adapter flows and widens the storage type to includestringfor theuserCodeindex; adds regression tests. - Accessibility — dialog titles/descriptions and a missing l10n key (#7835 / #7836). Adds the
index.codekey referenced byindex.htmlbut never defined (which produced a "Couldn't find translation key" console error on the landing page), and gives every admin@radix-ui/react-dialogDialog.ContentaDialog.TitleandDialog.Description(visually hidden where there's no visible heading), silencing Radix's a11y warnings. A new backend spec fails CI if anydata-l10n-idinsrc/templates/*.htmlis missing fromen.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
packageManagerpin inpackage.json(pnpm 11.1.x enforces a minimum-release-age policy the frozen-lockfile build can't satisfy), pnpm treated every call — including the in...
Assets 6
v3.2.0
3.2.0
3.2 adds first-class reverse-proxy / ingress support — X-Forwarded-Prefix and X-Ingress-Path are now honoured under trustProxy, so Etherpad can live under a subpath (Traefik, Nginx, Kubernetes Ingress) without breaking the PWA manifest, social-meta URLs, or any of the bootstrap asset links. The admin settings page learns to show resolved runtime values next to ${VAR:default} placeholders, the v3.1.0 admin pad-list filter chips now apply server-side (so "show empty pads" no longer returns 0–12 of hundreds), and the v3.1.0 redesigned outdated-version gritter actually fires in production now (the session-based author lookup it shipped with always returned null for pad visitors).
Notable enhancements
- HTTP — accept
X-Forwarded-PrefixandX-Ingress-PathundertrustProxy(#7802 / #7806). WithtrustProxy: true, Etherpad now honoursX-Forwarded-Prefix(de-facto Traefik / Spring) andX-Ingress-Path(Kubernetes Ingress) in addition to the prefix it already inferred from the request path. The sharedsanitizeProxyPathhelper added in 3.1.0 (defence-in-depth:[A-Za-z0-9_./-]only,//+collapsed,..traversal rejected) is extended to the new headers and applied consistently across/manifest.json,socialMetaog:url/og:image, and theindex.html/pad.html/timeslider.html/export_html.htmltemplates (manifest links, jslicense links, reconnect URLs). A pre-existing..segment-count miscalculation inpad.html/timeslider.htmlthat broke the manifest link when served from a deep subpath is also fixed in passing. New end-to-end suite covers the prefix-applied / prefix-ignored matrix undertrustProxy=true|falsefor both header names.settings.json.templatedocuments the new headers alongside the existingtrustProxynotes. - Admin settings — resolved runtime values surface on env-pill chips (#7803 / #7807). The
/admin/settingssocket payload now carries a newresolvedfield alongside the existing raw-fileresultsblob, carrying the actual in-memory settings module run through a new redactor (AdminSettingsRedact) that replaces known-sensitive paths (users.*.password,dbSettings.password,sso.clients[*].client_secret,sessionKey, ...) with[REDACTED]. The admin SPA'sEnvPillrenders a→ active valuechip when the path is resolved, or→ ••••••with a redacted tooltip when the server returned the sentinel — soport: ${PORT:9001}now shows→ 9001(or whatever the live value is) instead of silently falling back to the template default. Old admin SPAs that don't readresolvedcontinue to work; the save round-trip is unchanged so${VAR:default}literals are still preserved verbatim on disk. The admin test script glob picks up.test.tsxalongside.test.tsso the newEnvPillandresolveByPathtests run undertsx --test.
Notable fixes
- Admin pads — filter chip now applies server-side, before pagination (#7798). The 3.1.0 admin pad-list filter chips (
active/recent/empty/stale) ran on the client after the 12-row page slice had already arrived. On a deployment with hundreds of pads, clicking "empty pads" on page 1 only matched the 0–12 empties that happened to land in the current page, with the pagination footer reporting nonsense totals (reported on a 3.1.0 deployment). The filter is now part of thepadLoadsocket query — pattern filter on names runs first (cheap), metadata hydration for the matching pad universe is gated on a non-allfilter or a non-padNamesort and runs under a 16-way concurrency cap (was unboundedPromise.all, which fanned out to thousands of in-flightpadManager.getPad()reads on busy deployments), then the filter chip, then sort + slice.totalreflects the filtered universe so the footer makes sense. Older admin clients that don't sendfilterkeep working — the server defaults toall. Theif/else ifladder that duplicated the hydrate-and-sort loop persortByis folded into one pipeline with a single comparator switch. - Pad outdated notice — author now resolved from token cookie, not session (Qodo #7804 / #7805). The 3.1.0 redesigned outdated-version gritter never fired in production.
resolveRequestAuthor()looked for anauthorIDonreq.session.user, which Etherpad does not populate for pad visitors (express-session only carries the admin-login user), socomputeOutdated()always returned EMPTY. The lookup now mirrors how the socket.io handshake resolves pad-visitor identity — read the HttpOnlytoken(or<prefix>token) cookie and callauthorManager.getAuthorId(token, user)via a dynamic import (same circular-init guard pattern the file already uses forPadManager). The admin OpenAPI document gains adescriptionnote clarifying that/api/version-statusis a public pad-side endpoint that lives in the admin doc only because it shares the same internal route registration. - Localisation — silence spurious "could not translate element content" warning (#7797).
<select data-l10n-id="...">with<option>element children — the pattern used byep_headings2,ep_align,ep_font_size,ep_font_family, ... — used to drop into the textContent branch ofhtml10n.translateNode, hunt for a text-node child to overwrite, find none, and emitUnexpected error: could not translate element content for key ...on every pad load. TheSELECT/INPUT/TEXTAREAaria-label fallback already lived inside the same else-branch after the warning, so the accessible name landed correctly but the noisy console line still fired. Form-control elements now short-circuit into the aria-label path before the text-node hunt — aria-label is the only sensible localization target for these elements (a<select>'s text is its<option>labels, not its own name). Closes the console warning reported on Etherpad 3.1.0.
Internal / contributor-facing
- CI — swap archived
ep_readonly_guestforep_guestin the plugin matrix (#7795 / #7808).ep_readonly_guestis archived (read-only on GitHub) and itsauthenticatehook unconditionally swappedreq.session.userwith a read-only guest, even when the request carried an HTTP Authorization header. That silently demoted admin login attempts and stalled theanonymizeAuthorSockettests for 14 min/run on every with-plugins CI matrix. The pre-fix theory from 3.1.0 (#7796) blamedep_hash_auth.handleMessage; that was a red herring —handleMessageonly fires on the/padnamespace, never on/settings.ep_guestis the maintained successor (same authors, same purpose); 1.0.72 on npm already defers to basic auth / admin paths. Swapping the matrix unblocks theanonymizeAuthorSocketsuite on Linux, Windows, and the upgrade-from-latest-release workflow. The runtime probe added in #7796 stays — it still catches any other authenticate-hook plugin that rejects the test's plain-text credentials (e.g. a future hashed-only plugin). - Tests — admin
saveSettingsround-trip + cross-restart persistence (#7819 / #7820 / #7821). The adminsaveSettingssocket had zero direct backend coverage and the existing e2e "restart works" test only checked that the page renders after a restart, neither of which catches a deployment that resetssettings.jsonon restart, nor the user-visible workflow that triggered #7819 (add a top-level plugin block via Raw, save, watch it disappear). Three new backend specs (adminSettingsSave.ts) verify byte-for-byte write, top-level-block augmentation round-tripping through the nextload, and/* */comments surviving the write path. A new e2e spec mirrors the #7819 user workflow — open Raw, prepend anep_oauth-shaped top-level block, save,restartEtherpad(), re-login, confirm the block is still in Raw and surfaces as its own Form-view section (Ep oauthfromhumanize()). A separatedocker.ymljob (adminSettings_7819.ts) authenticates viaPOST /admin-auth/(always-requireAdmin, regardless ofsettings.requireAuthentication), saves a hand-built minimal-but-viable settings document containing a marker block,docker exec test greps for it,docker restarts the container, waits for the health probe, and re-greps. Both checks must pass. - Bug report template now asks contributors whether the abstraction in their proposed fix matches the rest of the codebase, to head off premature-generalisation fixes earlier in review.
Dependencies
ueberdb26.0.3 → 6.1.2 (two patch releases of cleanup on top of the 6.1.0findKeysPagedAPI that the 3.1.0 sessionstorage OOM fix relies on).semver7.8.0 → 7.8.1,lru-cache11.3.6 → 11.5.0,@elastic/elasticsearch9.4.0 → 9.4.1,pg8.20.0 → 8.21.0,openapi-backend5.16.1 → 5.17.0,tsx4.22.0 → 4.22.3,@tanstack/react-query5.100.10 → 5.100.11 +@tanstack/react-query-devtools,js-cookie3.0.6 → 3.0.7, plus two dev-dependency group bumps.
Localisation
- Multiple updates from translatewiki.net.
Assets 6
v3.1.0
3.1.0
3.1 ships the self-update programme's Tier 4 — autonomous in a maintenance window for real (the v3.0.0 notes documented the design; this is the release the code actually lands in), adds first-class SMTP delivery so update failures email the admin, and bundles a defence-in-depth pass across the HTTP/API entry points. Two new admin-facing escape hatches arrive: a preflight check that aborts an update before it mutates the working tree when the target tag's engines.node doesn't match the running runtime, and email notifications for every auto-rollback / preflight outcome (not just the terminal rollback-failed state).
Notable enhancements
- Self-update — Tier 4 (autonomous in a maintenance window). Set
updates.tier: "autonomous"together withupdates.maintenanceWindow: {"start":"HH:MM","end":"HH:MM","tz":"local"|"utc"}to constrain autonomous updates to a nightly window. The scheduler snapsscheduledForforward to the next window opening when grace would otherwise land outside the window, and defers the fire when the window has closed by the timer callback. Cross-midnight windows (end < start) are supported; DST transitions are absorbed by host wall-clock arithmetic. A missing or malformed window degrades the policy to Tier 3 with an explicitpolicy.reasonofmaintenance-window-missing/maintenance-window-invalid; an admin banner surfaces the misconfiguration so autonomous behaviour is not silently disabled. The admin update page shows a "Maintenance window" section with the parsed window summary, the next opening, and a "deferred until " subtitle on the scheduled panel when the timer has been snapped forward. Closes #7607 (#7753). - Updater — real SMTP via nodemailer (new top-level
mail.*block). Replaces the "(would send email)" stub. New settings:mail.host,mail.port,mail.secure,mail.from,mail.auth.{user,pass}.mail.host=nullkeeps the legacy log-only behaviour. Thenodemailerdependency is lazy-imported on first send so installs that don't configure mail pay no runtime cost; the transport is cached on the full SMTP options tuple so areloadSettings()change to host/port/credentials invalidates the cache.settings.json.dockerreadsMAIL_HOST/MAIL_FROM/MAIL_PORT/MAIL_SECUREfrom env. Send errors are logged warn and swallowed so a transient SMTP failure can never poison the updater state machine. - Updater — preflight against the target tag's
engines.node. Before mutating the working tree,runPreflightnow runsgit show <tag>:package.jsonand verifiesprocess.versions.nodesatisfies the target'sengines.node. A mismatch fails cleanly atpreflight-failedwith the detailtarget requires Node >=X, running Y— no drain, no restart, no rollback. The check runs after signature verification so we only trust signedpackage.json. NewPreflightReason: 'node-engine-mismatch'. - Updater — email admin on rollback / preflight-failed (not just
rollback-failed). Before this release only the terminalrollback-failedstate emailed. Auto-recovered failures (rolled-back-install-failed,rolled-back-build-failed,rolled-back-health-check,rolled-back-crash-loop) andpreflight-failednow also fire one email per<outcome>:<targetTag>(dedupe key inEmailSendLog.lastFailureKey). A 3am autonomous update that rolls back because of, say, a Node engine bump now lands in the admin inbox at 3am instead of staying invisible until the next admin login. Boot-path catch-up covers cases where the failure preceded a clean process exit (timer-fired health-check rollback, crash-loop forced rollback, preflight-failed that didn't get to email before exit). - API —
listAuthorsOfPadfilters the synthetic system author.Pad.SYSTEM_AUTHOR_ID(a.etherpad-system) is the placeholder Etherpad attributes to when the HTTP API receives a call without anauthorId(setText, setHTML, appendText, server-side import). It was leaking throughlistAuthorsOfPad, making pads with only API-driven content appear to have one "real" author. The synthetic id is now filtered at that API surface only —getAllAuthors()and downstream callers (copy, anonymize, atext verification) still see it. Fixes #7785 / #7790 (#7793).
Notable fixes
- Export HTML — ordered-list counter no longer poisoned by a sibling unordered list. When an ordered-list level was the only consumer of
olItemCounts, closing any list at that depth (including a<ul>that happened to share the level) reset the counter to 0. A subsequent unrelated<ol>at the same depth then took the "counter exists but is 0" branch and emitted<ol class="...">without thestart=attribute. The reset is now gated online.listTypeName === 'number'so closing an unordered list never touches the ol bookkeeping. Fixes #7786 / #7787 (#7791). - Export — bad
:revreturns a meaningful 500 body, not Express's HTML error page. A non-numeric:rev(e.g./p/foo/test1/export/txt) reachedcheckValidRevwhich throwsCustomError('rev is not a number', 'apierror'); the message fell through.catch(next)and Express's default renderer returned an HTML 500 page. The route handler now catches the apierror and emitserr.messageas a deterministictext/plain500. As a follow-up,checkValidRevruns beforeres.attachment()so an invalid rev no longer leaves aContent-Dispositionheader in place (browsers were offering to save the error message as a file), and unrelated export failures (conversion, fs, soffice) are surfaced as text/plain rather than the HTML stack page. Fixes #7788 (#7792).
Security hardening
A bundle of defence-in-depth tightening picked up during an internal audit pass (#7784):
- HTTP API — OAuth JWT path. Verify the signature before reading any claim off the payload; require
admin: truestrictly (presence is no longer sufficient). The apikey comparison switches tocrypto.timingSafeEqual. - Import/Export temp-file path tokens. Derived from
crypto.randomBytes(16)instead ofMath.random(). - Token transfer. Records now have a 5-minute TTL and are single-use (removed from the store before responding). The author token is no longer in the redemption response body — the
HttpOnlycookie is the only delivery channel. x-proxy-pathheader sanitiser (newsrc/node/utils/sanitizeProxyPath.ts). Shared byadmin.tsandspecialpages.ts. Strips characters outside[A-Za-z0-9_./-], collapses leading//+to a single/, rejects..traversal.admin.tsalso emitsVary: x-proxy-pathandCache-Control: private, no-storeso a poisoned response can never be reused for another origin.Pad.appendRevisioninsert-op author invariant. Centralises the "every insert op carries anauthorattribute" rule the socket handler already enforced, so non-wire callers (setText,setHTML,restoreRevision, plugin paths) get the same check.Pad.initandsetPadHTMLsubstituteSYSTEM_AUTHOR_IDwhen no author is supplied — same patternsetText/spliceTextalready used.setPadRawlegacy-import rewrite. Bulk-import bypassesappendRevision, so a hand-crafted.etherpadfile could persist non-conforming records that any subsequentsetText/setHTMLwould refuse to extend. A pre-pass now walks revs in order, sanitises each changeset's+ops against the cumulative pad pool (substitutingSYSTEM_AUTHOR_IDwhere needed), and re-applies each changeset to a running atext so the head atext and key-revmeta.atext/meta.poolsnapshots stay in lock-step. Conforming payloads round-trip unchanged.
Internal / contributor-facing
- Backend tests —
tests/backend/specs/{api,admin}/*un-skipped. The pnpm test script's glob (tests/backend/specs/**.ts) only matched depth-1 files. Every spec underapi/(14 files) andadmin/(2 files) has been silently skipped by CI. Switched to--extension ts --recursiveso mocha walks the tree as documented. A new vitest regression check reads the pnpm script, hands mocha the same arguments under--dry-run --list-files, and asserts representative specs from both subdirectories appear in the discovered list (#7789). - CI — Windows
npx ENOENTin the glob-discovery regression check.execFileSync('npx', ...)doesn't pick upnpx.cmdon Windows runners. Resolved by runningmocha's JS entry directly viarequire.resolveunder the current node process. Path normalisation now goes throughpath.relative+replace([\\/])so mixed-separator / drive-letter casing on Windows mocha output still matches the POSIX-relative assertions (#7794). - CI —
anonymizeAuthorSocketsuite gated on admin-socket health whenep_hash_authis installed. Un-hiding the suite in #7789 surfaced a 14-minute stall on every with-plugins matrix run becauseep_hash_auth'shandleMessagehook fires for every socket message regardless of namespace and reads from the deprecatedclientcontext (undefined for non-pad namespaces). Until the root cause lands (tracked in #7795), the suite skips itself when an application-level probe shows the admin/settingsnamespace isn't responding — keeps the no-plugin matrix covered and stops burning ~14 minutes per with-plugins run (#7796).
Localisation
- Multiple updates from translatewiki.net.
Assets 6
v3.0.0
3.0.0
3.0 is a feature-heavy release that closes out the self-update programme (Tiers 2 and 3 land alongside Tier 1 from 2.7.3), removes the last identified upstream telemetry vector, and ships a parsed JSONC settings editor, native DOCX export, in-place pad history scrubbing, and an admin UI for GDPR author erasure. It also marks the start of the broader Etherpad app ecosystem (see Companion apps below).
Breaking changes
- Minimum required Node.js version is now 24. Node.js 22 is no longer supported. Node 25 was briefly the floor mid-cycle but was rolled back to 24 LTS (Krypton, supported through ~May 2028) because Node 25 reached end-of-life on 2026年04月10日 (see #7779 / #7781). The CI matrix targets Node 24 and 26. Node 24 still ships Corepack, so existing
bin/installer.sh/bin/installer.ps1flows continue to work unchanged; the globalpnpminstall fallback added for the Node 25 detour is kept for forward-compatibility. pnpmfloor raised topnpm@11.1.2.packageManageris now pinned topnpm@11.1.2andengines.pnpmrequires>=11.1.2. The Dockerfile, snap, .deb and all GitHub workflows are aligned.swagger-ui-expressremoved./api-docsnow serves a vendored, telemetry-free copy of Scalar (see the privacy item below). The route, the OpenAPI document, and the rendered output are unchanged for downstream consumers, but anything that introspectedswagger-ui-expressinternals will need updating.- Debian package depends on
nodejs (>= 24). The signed apt repository atetherpad.org/aptis rebuilt against this floor; older Node packages are no longer acceptable as a dependency (#7754).
Companion apps
This release coincides with the launch of two ecosystem projects, both maintained under the ether org and able to talk to any 3.x Etherpad server over its existing HTTP / WebSocket API:
ether/etherpad-desktop— a native desktop wrapper around Etherpad for macOS, Windows and Linux. Single-window editor experience, system-tray indicator, and an optional embedded server for fully offline pads.ether/pad— a portable cross-target client: an Android and iOS app for editing pads on the go, and anano-style terminal client for headless / SSH workflows. Shares the same realtime client transport as the browser editor so changes propagate live across desktop, mobile, terminal and the web UI.
Both clients hit the stable 3.x API surface, so server operators don't need to enable anything extra to support them — the OpenAPI clean-up landed in this release (see Notable enhancements) is what makes the shared client code generators viable.
Notable enhancements
- Self-update subsystem — Tier 2 (manual click).
- Admins on a git install can click "Apply update" at
/admin/update. Etherpad runs a 60s session drain (with T-60 / T-30 / T-10 broadcasts to every pad),git fetch / checkout / pnpm install --frozen-lockfile / pnpm run build:ui, and exits with code 75 so a process supervisor restarts it on the new version. The next boot runs a 60s health check; if/healthdoesn't come up the previous SHA + lockfile are restored automatically. - Crash-loop guard: if the new version reboots more than twice without the health check completing, RollbackHandler forces a rollback regardless of the timer.
- Terminal
rollback-failedstate surfaces a strong banner; the admin clicks Acknowledge once they've manually recovered to clear the lock and re-allow Tier 2 attempts. - New settings under
updates.*:preApplyGraceMinutes,drainSeconds,rollbackHealthCheckSeconds,diskSpaceMinMB,requireSignature,trustedKeysPath. Tag signature verification is opt-in (defaultfalse) — seedoc/admin/updates.mdfor the keyring setup. - A process supervisor (systemd / pm2 / docker
--restart=unless-stopped) is required to apply updates. Without one, exit 75 leaves the instance down.
- Admins on a git install can click "Apply update" at
- Self-update subsystem — Tier 3 (auto with grace window).
- On a git install, set
updates.tier: "auto"to have new releases applied automatically afterpreApplyGraceMinutes. During the grace window,/admin/updateshows a live countdown plus Cancel and Apply now buttons. Schedules are persisted tovar/update-state.json, so an Etherpad restart during the grace window rehydrates the timer instead of losing the schedule. A new release tag detected mid-grace re-arms the timer; ifadminEmailis set, a one-shotgrace-startnotification fires per scheduled tag (issue #7607). - The terminal
rollback-failedstate continues to disable auto/autonomous attempts globally until acknowledged; manual click stays available because an admin click is the intervention the terminal state requires. - Tier 4 (autonomous in a maintenance window) remains designed but unimplemented and will land in a subsequent release.
- On a git install, set
- Privacy — drop swagger-ui telemetry, document phone-homes, add opt-outs.
- Dropped
swagger-ui-expressbecause upstream injects a Scarf analytics pixel that cannot be disabled at install or runtime (see swagger-api/swagger-ui#10573)./api-docsnow serves a vendored copy of Scalar (MIT) configured withwithDefaultFonts: falseandtelemetry: falseso no outbound calls are made. - New
privacy.updateCheck(defaulttrue) — set tofalseto disable the hourlyUpdateCheck.tsrequest to${updateServer}/info.json. - New
privacy.pluginCatalog(defaulttrue) — set tofalseto disable the admin plugins page fetch of${updateServer}/plugins.json. CLI install-by-name still works. - New
PRIVACY.mdat repo root documenting both outbound calls, what they send, and how to turn each off. bin/plugins/stalePlugins.tsnow readssettings.updateServer(was hardcoded tostatic.etherpad.org) and honours the new flag.- Closes #7524.
- Dropped
- Parsed JSONC settings editor in
/admin. The settings page now parsessettings.jsonas JSONC (with comments and trailing commas preserved), validates edits in-browser, and writes the file back without clobbering comment blocks (#7709, closes #7603, takes over #7666). - GDPR — admin UI for author erasure. Builds on the 2.7.3 author-erasure API: admins can now find an author by id or name in
/adminand run a confirmed erasure flow from the UI (#7667, follow-up to #7550). - Pad-wide settings on by default.
padOptions-style settings can now be edited from the in-pad cog without flipping a flag, and the modal title no longer misleads about scope (#7679). Plugin-namespacedep_*keys also flow throughapplyPadSettingsso plugins can register their own pad-wide options (#7698). - Scrub history in-place on the pad URL. A long-edited pad can now have its history rewritten in place (e.g. for compliance or to drop accidentally-pasted secrets), without changing the pad URL or breaking deep-links (#7710, closes #7659).
bin/compactStalePads— staleness-gated bulk compaction. Companion to the 2.7.3compactAllPadsCLI: targets only pads not edited in the last--older-than Ndays, so hot pads in active timeslider use are left alone. Same--keep/--dry-runshape ascompactAllPads(#7708, issue #7642).- Native DOCX export (opt-in). A
html-to-docx-based exporter lands as an alternative to the LibreOffice path, so installs that don't wantsofficeon the host can still produce.docx.sofficeis now documented as optional for.docxand.pdf(#7568 / #7707, issue #7538). - Editor / UI.
- Settings popup is now scrollable on short viewports so the lower controls stay reachable on small laptops (#7703, issue #7696).
- Admin design pass cleans up the rework introduced in 2.7.3 (#7716).
theme-colormeta now follows the client-side dark-mode switch instead of locking to the boot-time value (#7690, issue #7606).menu_rightstays visible on readonly pads by default; operators that prefer the slimmer chrome can still opt in viashowMenuRight(#7783).- Social meta: new
settings.socialMeta.descriptionoverride (#7691) plus a fix for numeric / boolean override values that were silently being dropped during coercion (#7692).
- Admin / API surface.
- The published OpenAPI spec is cleaned up for downstream codegens — duplicate operationIds removed, response schemas filled in,
nullable⟶oneOf nullmigrated for OpenAPI 3.1 (#7714). The companion apps above consume this directly. - Admin endpoints (
/admin/*JSON APIs) are now documented in the OpenAPI spec (#7693 / #7705) and called from a typesafe TanStack Query client in the admin SPA (#7638 / #7695). - "Requires newer Etherpad" message in the plugin browser when an
ep.jsondeclares anengines.etherpadhigher than the running version, instead of failing with a generic install error (#7763 / #7771).
- The published OpenAPI spec is cleaned up for downstream codegens — duplicate operationIds removed, response schemas filled in,
- Security hardening.
- Reject
USER_CHANGESinserts that arrive without an author attribute, closing a server-side trust gap where unattributed changes could be applied to a pad (#7773). - Integrator-issued
sessionIDcookies can now be markedHttpOnlyvia the new option, matching the 2.7.3 author-token hardening (#7045 / #7755).
- Reject
- Observability — Prometheus counters. Three new counters surface scaling-relevant events (
pad_load_total,socket_connect_total,changeset_apply_total) so operators can drive horizontal-scaling decisions off the existing/metricsendpoint without a custom exporter (#7756 / #7762). - Accessibility (continuation of the 2.7.2 / 2.7.3 pass).
Assets 6
v2.7.3
2.7.3
Breaking changes
- Minimum required Node.js version is now 22.13. Node.js 20 is reaching end-of-life (see https://nodejs.org/en/about/previous-releases) and pnpm 11 hard-rejects Node releases older than 22.13. The CI matrix targets Node 22, 24, and 25. Upgrading should be straightforward — install a current Node.js release before updating Etherpad.
- The official Docker image no longer ships
curl,npm, ornpx. These were dropped to remove transitive CVEs (curl/libcurl SMB advisories, npm's bundled picomatch 4.0.3 and brace-expansion 2.0.2). The container's healthcheck now useswget(busybox built-in, always present), and Etherpad provisionspnpmviacorepackfor all runtime package operations. If you exec into the container and rely oncurlornpmfor ad-hoc tasks, install them on demand withapk add curlor use the busyboxwget/pnpmalready present.
Notable enhancements
- GDPR / privacy controls. A multi-PR series adds the building blocks operators need to satisfy data-subject requests:
- Pad deletion controls (admin-driven and self-service).
- IP / privacy audit pass across the codebase.
- Author-token cookies are now
HttpOnly, removing them from JavaScript reach. - Configurable privacy banner shown on first visit.
- Author erasure: an authenticated path for purging an individual author's identity and contributions.
- Self-update subsystem (Tier 1: notify).
- Periodic check against the GitHub Releases API for the configured repo (default
ether/etherpad). Configurable via the newupdates.*settings block, default tier"notify". Setupdates.tierto"off"to disable entirely. - The admin UI shows a banner and a dedicated "Etherpad updates" page with the current version, latest version, install method, and changelog.
- Pad users see a discreet footer badge only when the running version is severely outdated (one or more major versions behind) or flagged as vulnerable in a recent release manifest. The public endpoint that drives this never leaks the version string itself.
- New top-level
adminEmailsetting. When set, the updater emails the admin on first detection of severe / vulnerable status, with escalating cadence (weekly while vulnerable, monthly while severely outdated). PR 1 ships the dedupe + cadence logic; real SMTP wiring lands in a follow-up PR. - Tier 1 ships in this release. Tiers 2 (manual click), 3 (auto with grace window) and 4 (autonomous in maintenance window) are designed and will land in subsequent releases.
- See
doc/admin/updates.mdfor full configuration.
- Periodic check against the GitHub Releases API for the configured repo (default
- Pad compaction. New
compactPadHTTP API plusbin/compactPadandbin/compactAllPadsCLIs to reclaim database space on long-lived pads with heavy edit history (issue #6194).--keep Nretains the last N revisions;--dry-runpreviews per-pad rev counts before writing. Per-pad failures don't stop the bulk run. - New packaging targets.
- Etherpad is now published as a Snap package.
- Debian (.deb) packages are built via nfpm with a systemd unit, and a signed apt repository is published to
etherpad.org/apt.
- Editor enhancements.
- IDE-style line operations: keyboard shortcuts to duplicate or delete the current line.
- New
showMenuRightURL parameter to hide the right-side toolbar — useful for embeds that need slimmer chrome. - Click a user in the userlist to open chat with
@<name>prefilled, making mentions discoverable. - New
padOptions.fadeInactiveAuthorColorssetting plus a toolbar UI to fade the background colors of authors who have left the pad.
- Color contrast. Author colors now pick the WCAG-higher-contrast text color for readability.
- Social / mobile metadata. Pad, timeslider, and home views now emit Open Graph and Twitter Card tags (closes #7599) and a
theme-colormeta that matches the toolbar on mobile. - Plugin admin UX. The
/adminplugin browser surfaces each plugin'sep.jsondisablesdeclarations, so operators can see what a plugin will turn off before installing.
Notable fixes
- Socket.io: don't kick authenticated duplicate-author sessions. A regression where two tabs from the same authenticated author could evict each other has been fixed (#7656 / #7678).
- Anchor scrolling. Anchor-link navigation now waits for layout to settle, so jumping to a deep link no longer overshoots.
- Plugin updater.
bin/updatePlugins.shactually updates installed plugins again (closes #6670). - Settings: stable per-release version string.
randomVersionStringis now derived from the release identity rather than regenerated on each boot, so caches behave correctly across restarts of the same version.
Internal / contributor-facing
- The HTTP client in the backend has been migrated from
axiosto the built-infetchAPI, dropping a dependency now that Node 22 ships a stable fetch. admin/andui/workspaces moved fromrolldown-viteto upstream Vite 8.- Build and CI moved to pnpm 11 (
packageManager: "pnpm@11.0.6"); theDockerfile, snap, and all GitHub workflows are aligned. pnpm overrides have been migrated frompackage.jsontopnpm-workspace.yamlto match pnpm 11's expectations. - All client modules have been converted to ESM.
- The CI matrix tests Node 22, 24, and 25; on PRs the matrix is reduced to a single Node version to keep feedback fast.
- Frontend Playwright tests now run against the
/etherplugin set, with feature-tag based skips so plugin-incompatible specs are excluded automatically. - Build hardening: signed apt repo publishing, frozen lockfile installs across CI, Node setup pinned in every workflow, and a Docker-image CVE sweep that bumps
npm,pnpm, anduuid.
Localisation
- Multiple updates from translatewiki.net.
Assets 6
v2.7.2
Notable enhancements and fixes
- Accessibility pass: corrected dialog semantics, improved focus management, added missing icon labels, and set the
html langattribute correctly. - Chat: clicking the chat icon works again, disabled toggles render properly, and the username layout no longer overflows.
/export/etherpadnow honors the:revURL segment, so revision-specific exports return the requested revision instead of the latest.- Undo / redo now scrolls the viewport to follow the caret, so reverted edits stay in view.
- Page Down / Page Up now scrolls by viewport height instead of a fixed line count, matching standard editor behavior on tall and short windows alike.
- Editbar: caret is restored to the pad after changing a toolbar select, so typing continues in the document instead of falling through to the toolbar.
- Admin: i18n is restored on
/adminso the admin UI is translated again.
Assets 2
v2.7.1
2.7.1
Notable enhancements and fixes
- fixed stop harcoding lang=en, letting the client auto detect locale
- Stop mutating the shared plugin registry during sanitization
- Preserve non-breaking space
Assets 2
v2.7.0
Breaking changes
- Abiword has been replaced with LibreOffice for document import/export. If you were using Abiword for DOCX/ODT/PDF conversion, update your
settings.jsonto pointsofficeat your LibreOffice binary. DOCX export is now supported out of the box.
Notable enhancements
- Added line numbers to the timeslider so you can follow along with specific lines while replaying a pad's history.
- Added a playback speed setting to the timeslider — you can now scrub through history faster (or slower) than real time.
- Creator-owned pad settings defaults: the user who creates a pad now seeds its default settings, giving pad creators more control over initial configuration.
- Cookie names are now configurable via a prefix setting. Useful when running multiple Etherpads on the same domain and you need to keep their session cookies from colliding.
- Added a new
aceRegisterLineAttributeshook so plugins can preserve custom line attributes across Enter / line-split operations. Documentation for the hook is included. - Added a one-line installer script for getting Etherpad running quickly on a fresh machine.
- Docker images are now published to GitHub Container Registry (GHCR) in addition to Docker Hub.
- npm publishing of core and plugins has been migrated to OIDC trusted publishing for stronger supply-chain security.
Notable fixes
- Database drivers are now bundled with Etherpad again, so fresh installs no longer fail to connect to Postgres, MySQL, and friends out of the box. A regression test has been added to prevent this from breaking again.
- Pending changesets are now flushed immediately after a reconnect instead of being silently dropped, and users are warned when a pending edit is not accepted by the server.
- Head revision and atext are now captured atomically, preventing the occasional "mismatched apply" errors on busy pads.
- Clearing authorship colors can now be undone without forcing a client disconnect.
- Added periodic cleanup of expired/stale sessions from the database, and fixed a race condition in the session cleanup timeout.
- Error messages returned to clients are now sanitized by default with deduplication, so internal details no longer leak through error responses.
- Raised the maximum socket.io message size to 10 MB so large pastes no longer get rejected.
- Dev mode entrypoint paths now respect the
x-proxy-pathheader, fixing reverse-proxy setups in development. - Numerous list-related fixes: numbered list wrapped lines now indent correctly, ordered list numbering is preserved across bullet interruptions during export, consecutive numbering survives indented sub-bullets, switching from unordered to ordered resets numbering, and line attributes are preserved across drag-and-drop.
- Bold (and other) formatting is now retained after copy-paste.
- Dead-key / compose-key input no longer eats the preceding space.
POSTAPI requests with a JSON body no longer time out.appendTextnow correctly attributes the new text to the specified author.createDiffHTMLno longer fails withNot a changeset: undefined.- Added
padIdto thepadUpdate/padCreatehook context. - Fixed
numConnectedUsersto include the joining user in its count. - Accessibility improvements: keyboard trap fix, better screen reader support, and
aria-liveannouncements. - RTL URL parameter
rtl=falsenow correctly disables RTL mode. - Language dropdown is now sorted alphabetically by native name.
- PageDown now advances the caret by a full page of lines.
- ESM/CJS interop issues in the Settings module that had been breaking plugin compatibility have been resolved, with setters added to the CJS compatibility layer and regression tests in place.
- Several Docker build fixes: git submodule handling,
hardlinkpackage-import-method for ZFS, and production-only workspace config.
Other
- Many occurrences of "etherpad-lite" have been renamed to "etherpad" across the codebase and documentation.
- Pinned 33 transitive dependencies to patched versions to clear out Dependabot security alerts.
- Restricted
GITHUB_TOKENpermissions in the update-plugins workflow.
Assets 2
v2.6.1
2.6.1
For those wondering where the new updates are and why it was very quite throughout the last 1 1/2 years – I've been working on a new implementation of Etherpad from scratch in Go. It's called Etherpad-Go and you can find it here: https://github.com/ether/etherpad-go
and a short FAQ about it here: https://github.com/ether/etherpad-go/wiki/FAQ . I'd love to hear your feedback about it either on Discord or issue tracker. There is a README.md that explains how to get started and try it out and also the FAQ can be quite fruitful. Latest release can be found here: https://github.com/ether/etherpad-go/releases/tag/v0.0.4
Notable enhancements and fixes of this release
- Minor fixes and improvements to the session transfer feature introduced in 2.6.0
- Dependencies upgrades