# 3.3.16 3.3.16 is a security release. It fixes a race in the collaboration socket that could expose and write to pads the user was not allowed to open, and picks up the ueberDB MSSQL fix. Upgrade as soon as you can. ### Security - **Collaboration — every socket message now stays bound to the pad it was authorized for ([GHSA-c36r-65p6-q3m9](https://github.com/ether/etherpad/security/advisories/GHSA-c36r-65p6-q3m9), #8458).** The earlier fix for GHSA-6mcx-x5h6-rpw2 only pinned the target pad for USER_CHANGES. Chat, history (CHANGESET_REQ), saved revisions, pad options and pad deletion still read the pad from the socket's session when they ran. A second CLIENT_READY on the same socket rewrote that session before its own access check. A message authorized for one pad could then run against another, which leaked the other pad's text, history and chat and let chat messages and saved revisions be written into it, including on pads the user could only view. CLIENT_READY now changes the session only after access is granted, and each message carries the pad and access mode it was authorized with. Reported by @leograntquinn-crypto. - **Database — ueberdb2 6.1.19 (#8444).** Fixes SQL injection in the MSSQL driver's bulk writes ([GHSA-xmqq-59rx-x7pf](https://github.com/ether/ueberDB/security/advisories/GHSA-xmqq-59rx-x7pf)). Only installs using `"dbType": "mssql"` are affected. ### Other changes - The default-content author test no longer depends on the local `defaultPadText` setting (#8445). - The Docker workflow now records the release version (#8456). - Dependency updates, including undici 8.12.4, mssql 12.7.14, entities 9.0.0, surrealdb 2.2.2 and resolve 1.22.14. # 3.3.15 3.3.15 is a packaging release. It shrinks the Debian package so it can be published to the apt repository again. ### Notable fixes - **Debian package — the .deb is back under GitHub's 100 MB file limit (#8442).** The 3.3.14 apt publish failed because `etherpad_3.3.14-1_amd64.deb` was 101 MiB. The package build copied the whole workspace `node_modules`, devDependencies included. It now does a fresh production-only install, which brings the amd64 .deb down to about 46 MiB. The built admin UI still ships, and the build now fails if a .deb goes over 95 MiB. ### Other changes - CI now runs ep_ai_core and ep_ai_mcp in the with-plugins backend jobs (#8431). - Dependency updates, including nano 11.0.8, surrealdb 2.2.1, express-rate-limit 8.7.3, rate-limiter-flexible 11.2.4, semver 7.8.6 and @tanstack/react-query 5.105.3. # 3.3.14 3.3.14 is a bug-fix release. It fixes a SIGTERM that could be lost during startup, two updater bugs, a join race that disconnected editors under load, and the overlapping controls in history mode. ### Notable fixes - **Server — a SIGTERM sent during startup is no longer lost (#8421, #8423).** Startup removed the existing SIGINT/SIGTERM listeners before adding its own, so for a moment there were none and Node dropped a signal that had arrived while startup was busy. Etherpad then kept running until systemd sent SIGKILL. The new listener is now added before the old ones are removed. Reported by @ManUtopiK. - **Updater — Apply no longer fails on its own lock file, and a finished update shows as verified (#8411, #8412, #8425).** Apply took `var/update.lock` before checking for a clean git tree, so the check always saw the lock file and failed with `dirty-working-tree`. The check now ignores the files the updater writes itself. The first version check after boot also wrote the old `pending-verification` state back over `verified`, so the status stayed on "Pending verification". The state is now always read from disk, and a stale `pending-verification` resolves itself once Etherpad is running the target version. Reported by @damianbulira. - **Pads — a user joining no longer disconnects someone who is typing (#8387, #8409).** The catch-up sent to a newly joined client advanced the revision of every client on the pad. If another client's change was being saved at that moment, its revision check failed and it was disconnected with `badChangeset`, which caused reconnect cascades under collaboration load. The catch-up now only goes to the client that joined. Thanks to @Raj85446696, and to @MaBeFHDW for finding the cause. - **History mode — the follow (eye) icon no longer overlaps the playback speed control (#8382, #8410).** A generic colibris checkbox style also matched the follow toggle, adding padding and a switch that pushed the eye icon under the speed control and left it sitting too high. Those styles are now reset for the toggle. Reported by @meaz, thanks to @Jbansal2. ### Thanks to our security reporters Thanks to everyone whose privately reported security issues were fixed and published since mid-October. The fixes shipped in 3.3.3 to 3.3.13: - @barnessomer ([GHSA-qq7c-m9pc-7q7v](https://github.com/ether/etherpad/security/advisories/GHSA-qq7c-m9pc-7q7v)) - @sun1024 ([GHSA-35rj-7vrx-29wq](https://github.com/ether/etherpad/security/advisories/GHSA-35rj-7vrx-29wq)) - @sonicnew ([GHSA-x4mj-5635-3fq9](https://github.com/ether/etherpad/security/advisories/GHSA-x4mj-5635-3fq9)) - @CyberKareem ([GHSA-73h9-c5xp-gfg4](https://github.com/ether/etherpad/security/advisories/GHSA-73h9-c5xp-gfg4), [GHSA-wg58-mhwv-35pq](https://github.com/ether/etherpad/security/advisories/GHSA-wg58-mhwv-35pq), [GHSA-6mcx-x5h6-rpw2](https://github.com/ether/etherpad/security/advisories/GHSA-6mcx-x5h6-rpw2)) - @meifukun ([GHSA-pp5v-mvwg-76mp](https://github.com/ether/etherpad/security/advisories/GHSA-pp5v-mvwg-76mp)) - @gcm-explo1t and @neodymelabs ([GHSA-mc8w-wjhw-45x5](https://github.com/ether/etherpad/security/advisories/GHSA-mc8w-wjhw-45x5)) - @MaBeFHDW ([GHSA-746x-9w89-7xv6](https://github.com/ether/etherpad/security/advisories/GHSA-746x-9w89-7xv6)) - @baeseungwon1010 ([GHSA-g9xm-3pqr-wcph](https://github.com/ether/etherpad/security/advisories/GHSA-g9xm-3pqr-wcph)) ### Other changes - Dependency updates, including mongodb 7.8.0, jose 6.2.15 and @tanstack/react-query 5.105.2. # 3.3.13 3.3.13 is a security release. It stops `.etherpad` imports from setting a pad's ownership, privacy or deletion token (GHSA-qq7c-m9pc-7q7v). ### Security - **Import — `.etherpad` files can no longer change who owns a pad, make it public or plant a deletion token (GHSA-qq7c-m9pc-7q7v).** Importing a `.etherpad` file into a pad with fewer than 10 revisions wrote every `pad::*` record in the file and every field of the pad record. Anyone who could edit a new pad could replace the creator's deletion token with their own, make a private group pad public, or name themselves as the pad's creator. Imports now only take the pad text, revisions and chat. The pad keeps its public status, pad-wide settings come from the file only when the creator imports it, and a file that names a different creator can only be imported by the pad's creator. Reported by @barnessomer. # 3.3.12 3.3.12 is a security release. It closes an open redirect in the timeslider's "Return to pad" button (GHSA-35rj-7vrx-29wq) and stops revision-bound `.etherpad` exports from leaking later chat. ### Security - **Timeslider — "Return to pad" no longer follows a cross-origin referrer (GHSA-35rj-7vrx-29wq).** The button navigated to `document.referrer` whenever the character before its last `/` was `p`, so a link from `https://attacker/p/login` to `/p//timeslider?embed=1` sent the user back to the attacker's site when they clicked it. The referrer is now only followed when it is a pad page on the same origin; otherwise the button returns to the pad. Reported by @sun1024. - **Export — revision-bound `.etherpad` exports only include chat up to that revision.** Exporting `/p///export/etherpad` shipped the pad's entire chat history, including messages posted after that revision. Reported by @temantih. # 3.3.11 3.3.11 is a security release. It stops clients that connect without an author token from all sharing one author (GHSA-746x-9w89-7xv6). ### Security - **Pads — clients without an author token no longer share one author (GHSA-746x-9w89-7xv6).** Since the author token moved to an HttpOnly cookie, a pad connection that carried neither the cookie nor a legacy token resolved its token to `null`, and every such client was mapped to the same `token2author:null` author. Unrelated users (for example behind cookie blockers or in cross-site embeds) then appeared as one author and kicked each other off, and any of them counted as the creator of a pad another one had created, so they could delete it or change its pad-wide settings. A connection without a token now gets a fresh one for that session, and the access check does the same for HTTP callers such as import. Reported by @MaBeFHDW. # 3.3.10 ### Security - This is a security release to mitigate some CVEs recently discovered in Node (https://nodejs.org/en/blog/vulnerability/october-2026-security-releases). The recommended version is Node 24.22.1 and above # 3.3.9 3.3.9 is a security and feature release. It closes a path traversal in plugin uninstall (GHSA-g9xm-3pqr-wcph), adds built-in pad retention, and ships a batch of fixes. ### Security - **Plugins — dependency names are validated on uninstall as well as install (GHSA-g9xm-3pqr-wcph).** GHSA-92hr-gmr6-h8cp made the plugin installer validate dependency names on install, but the uninstall path still passed the raw `package.json` dependency key to `rmSync`/`unlinkSync`. A plugin declaring a dependency named like `zzz/../../..` could therefore delete a directory outside the plugin folder when an admin uninstalled it. Every install and uninstall path now resolves dependency paths through one helper that refuses names that escape the plugin directory. Exploiting it needs an admin to install and then uninstall the malicious plugin. Reported by @baeseungwon1010. ### New features - **Pad retention — delete pads nobody has edited for N days (#8340).** A new `padRetention` setting replaces the unmaintained `ep_delete_after_delay` plugin without its HTML archive copies or tombstone pads. It is off by default and, once enabled, only logs `would delete pad ` until `dryRun` is set to `false`. A periodic sweep reads each pad's last-edit time straight from the database without loading pads into the cache, and an overdue pad is also deleted when someone opens it instead of showing stale content. Deletion uses the same path as the `deletePad` API. Options: `deleteAfterDays`, `warnBeforeDays`, `sweepIntervalHours`, `excludeGroupPads` and `excludePattern`. - **Settings — hide the deletion-token modal without widening deletion rights (#7996, #8319).** The new `suppressPadDeletionTokenModal` setting (`SUPPRESS_PAD_DELETION_TOKEN_MODAL` in the container image) stops the one-time "Save your pad deletion token" modal from appearing. Until now the only way to hide it was `allowPadDeletionByAllUsers: true`, which also let any editor delete the pad. The token is still issued and still works, and non-creators still can't delete. Thanks to @sirsqm. ### Notable fixes - **Performance — fonts are no longer inlined into the minified CSS (#8268, #8314).** The pad stylesheet embedded every editor and icon font as base64, so `pad.css` was about 1.7 MB and every visitor downloaded all of it before the pad rendered. Fonts are now fetched separately and only when used, and `pad.css` is about 61 KB. Thanks to @sirsqm. - **Toolbar — a popup opened from the toolbar keeps focus (#8345).** The editor grabbed focus back after every toolbar command, so in current Firefox Escape didn't close the settings or users popup and typing a name didn't reach the name field. - **Colibris — dark mode colours apply everywhere (#8271).** The light `--text-color` and `--bg-color` values could override the dark ones on elements outside the editor, such as the history banner. Thanks to @bobmullut. - **Session transfer — failed requests are reported as failures (#8172, #8273).** Errors from the session transfer endpoint were treated as success. They now show a localized error, and a retry clears it. Thanks to @toashu123. - **Share dialog — "Read only" is now "Use read-only link" (#8181, #8326).** The checkbox switches which link and embed code you copy; it never locked the pad. The new label and an accessible description say so. Thanks to @jum-apzn. - **Docker — `--build-arg PnpmVersion` is honoured again (#8210, #8320).** Thanks to @sirsqm. ### Other changes - Localisation updates from translatewiki.net. - Dependency updates, including express 5.3.0, ejs 7.0.1, nodemailer 10.1.1, surrealdb 2.1.1, mssql 12.7.9 and ueberdb2 6.1.18. # 3.3.8 3.3.8 is a security release. It closes an HTTP API privilege escalation for instances using the built-in OIDC provider with API clients (GHSA-38vj-95q4-gwvx). ### Security - **HTTP API — client_credentials tokens are identified by a signed grant marker (GHSA-38vj-95q4-gwvx).** The API decided whether a bearer JWT came from the OAuth2 `client_credentials` grant by checking whether its `sub` claim equalled a configured `client_id`, and `client_credentials` tokens skip the `admin === true` claim check. A non-admin user whose account name collided with a configured client ID could therefore use an ordinary login token to get admin access to the HTTP API. This is a variant of GHSA-qfmh-fph3-mw8q. The provider now signs an `etherpad_grant: "client_credentials"` marker into client_credentials access tokens, and `extraParams` cannot override it. The API trusts that marker instead of comparing `sub` to client IDs. **Upgrade note:** client_credentials tokens issued before the upgrade don't carry the marker and are refused with 401 until they are re-issued. These tokens are short-lived. Reported by Yves Soete of Blacksight LLC (@yssoe). ### Notable fixes - **OIDC — numeric and boolean admin passwords work again (#8263, #8327).** The settings loader coerces environment-variable values, so `ADMIN_PASSWORD=123456` (for example via `settings.json.docker`) arrives as the number `123456`. The 3.3.6 login check (GHSA-62cj-9j72-mfrh) only accepted string passwords, so these admins could no longer log in. Finite numbers and booleans are now compared as strings. Missing, empty, NaN and non-scalar values are still refused. - **Settings — dropdowns open under `prefers-reduced-motion` (#8290, #8328).** With reduced motion enabled, the settings popup kept `transform: scale(1)`. That made the popup the containing block for the dropdown list, so the font and language lists rendered out of view. The popup now uses `transform: none`. Thanks to @kfogel. # 3.3.7 3.3.7 is a security release. It closes a stored XSS in the pad and timeslider renderer (GHSA-4mx2-rqx5-2pp6). ### Security - **Renderer — attribute-pool values can no longer forge class tokens (GHSA-4mx2-rqx5-2pp6, #8302).** `linestylefilter` appended the `start` and `list` line-attribute values verbatim to the space-delimited class string that `domline` parses back, and `domline` emits any `tag:` token in that string as a raw element. A `start` value containing a space, which can be planted through a crafted changeset or `.etherpad` import, therefore smuggled in a token such as `tag:img/src=x/onerror=...` and rendered a live element with a live event handler for every viewer of the pad and its timeslider. This is the same function as GHSA-f7h5-v9hm-548j but a different token. `start` is now emitted only when it is an integer and `list` only when it is a plain list type, and `domline` accepts only a bare element name (letters, digits and hyphens) for `tag:` tokens, which also covers class strings contributed by plugins. Reported by @arpitjain099. ### Notable fixes - **Auth — an empty-string password is refused on both login paths (#8261).** A `settings.users` entry configured as `"password": ""` authenticated anyone who submitted an empty password, on the OIDC interaction path and on HTTP Basic. Both already failed closed for a nullish password; an empty string slipped through because it is a string and compares equal to an empty submission. Only explicit misconfiguration produces it, so this is hardening rather than a vulnerability. Reported by Wenhao Wu (Southeast University) while verifying the fix for GHSA-62cj-9j72-mfrh. - **Admin — the plugin catalog no longer offers deprecated or known-broken plugins (#8246).** The "Available plugins" list was built straight from the plugin feed, so any package the feed knew about could be installed from the admin UI — including packages npm marks deprecated, packages the plugin registry itself could not get working against the current release, and `ep_adminpads2`, which is archived upstream and takes over `/admin/pads` with a template whose scripts core no longer ships, hanging the admin page on "Loading…". Those are now filtered out of the catalog, the admin UI refuses to install one if a stale page asks for it anyway (`pnpm run plugins i ep_` on the server still overrides), and an already-installed plugin in that state is flagged as deprecated in the *Installed plugins* list. The npm deprecation lookup is cached for 12 hours and fails open: if the registry cannot be reached the full catalog is still listed. Reported by @JohnMcLear. # 3.3.6 3.3.6 is a security and bug-fix release. It closes an OIDC login bypass for accounts configured without a password (GHSA-62cj-9j72-mfrh), and fixes a batch of reported defects across the installer, the admin settings editor, session transfer, the welcome screen, accessibility and plugin configuration. ### Security - **OIDC — refuse interactive logins for accounts without a password (GHSA-62cj-9j72-mfrh, #8247).** The embedded OpenID Connect provider compared the submitted password against `String(user.password)`, so a `settings.users` account with no `password` property compared against the literal string `"undefined"`, and one with `"password": null` against `"null"`. Submitting that literal logged the account in and issued a token carrying its `admin` claim; with the default `authenticationMethod: "sso"` the HTTP API accepts that token. Accounts without a usable password occur in practice — the container image leaves `password` null when `ADMIN_PASSWORD` is unset, and an `ep_hash_auth` entry replaces `password` with `hash`. The sibling HTTP Basic path in `webaccess.ts` already failed closed here; the credential check now lives in `verifyInteractiveLogin()` and refuses any account without a real string password rather than coercing a missing secret to a literal. Hash-only entries are refused on this path too, since they authenticate through the `authenticate` hook, which it does not consult. Reported by Wenhao Wu (Southeast University). ### Notable fixes - **PDF export honours `font-family` without LibreOffice (#8245, #8249).** The built-in PDF path used only pdfkit's Helvetica and Courier and ignored `font-family` entirely, so any font applied by a plugin such as `ep_font_family` was lost — while HTML, ODT and DOCX all carried it. Font families are now mapped onto the PDF standard fonts by category (sans-serif to Helvetica, serif to Times, monospace to Courier), including the bold and italic variants, honouring declaration order and `!important`. Exact non-standard faces can be supplied by an operator through the new `exportPdfFonts` setting, which points a family at TTF/OTF files; no fonts are bundled. Every failure path degrades with a warning rather than failing the export, and pads with no font styling export exactly as before. Family names arrive from pad content, so they are normalised and matched against an allow-list and are never used as a file path. - **Installer — the Node version check no longer fails under Windows PowerShell 5.1 (#8214, #8235).** `bin/installer.ps1` read the major version with `node -p 'process.versions.node.split(".")[0]'`. Windows PowerShell 5.1 — the default shell on Windows 10 and 11, and a version the script declares support for — strips the double quotes when passing arguments to a program, so Node received `split(.)[0]` and threw a `SyntaxError`. The empty result became 0, and the installer rejected every Node version as too old. The version now comes from `node --version` parsed in PowerShell, and an unparsable result reports that rather than claiming the version is too old. The Windows CI job now runs under both PowerShell 7 and Windows PowerShell 5.1. - **Admin — settings form fields honour escape sequences (#8211, #8239).** In the settings form view (raw mode was unaffected), string settings are edited in single-line inputs. Plain strings such as `defaultPadText` were rendered with literal newlines, which the browser silently strips from a single-line input, and whatever the user typed was escaped a second time on save, so `\n` was written as `\\n`. Environment-variable defaults such as `${DEFAULT_PAD_TEXT:...}` were shown escaped but escaped again on save. Both widgets now display values in escaped form and decode them before saving, so typing `Welcome\n\ntest\n` writes the same bytes as editing `settings.json` by hand. A half-typed escape is not saved: the field is marked invalid and reverts to the last saved value on blur. - **Session transfer — preferences survive the transfer, and the cookie is no longer double-encoded (#8171, #8238).** The transfer only handled the `prefsHttp` cookie, but over HTTPS the pad stores its preferences in `prefs`, so nothing was sent and the receiving side wrote a cookie the destination never reads. The client also sent the cookie still percent-encoded and `res.cookie()` encoded it again, leaving a value the destination pad could not parse, so it silently fell back to defaults. The server now reads the preferences from the request's own cookies (accepting either name, with or without the cookie prefix), accepts only a JSON object, and writes `prefs` or `prefsHttp` according to `req.secure`, encoded once. When there is nothing to transfer no cookie is written, so an existing destination preference set is no longer wiped. Author-token handling is unchanged. - **Session transfer — the dialog describes what actually happens (#8173, #8236).** The home-page dialog offered to copy a "link" that would move your "session". It copies a one-time code, valid once and for five minutes, that is pasted into the Receive session tab, and what moves is the author identity and preferences, not a sign-in session. The English wording of the existing strings now says so. - **Welcome screen — deleted pads leave the recent list (#8201, #8237).** The Recent pads list is stored in the browser and nothing ever removed an entry, so a deleted pad stayed listed and opening it silently created a new, empty pad under the same name. Clients now drop the pad from the list when the server announces the deletion, which covers the creator's own Delete pad action, a deletion performed with the recovery token from another device, and any other tab open on the pad. Names stored URL-encoded by older versions are matched too. Pads deleted through the HTTP API or the admin interface still linger in browsers that had no tab open on them, since the list is per-browser. - **Accessibility — screen readers can move through a pad line by line (#7778, #8240).** Every pad line is rendered as a plain `
` with no role, which browsers expose as an anonymous generic node, so assistive technology saw one flattened run of text with no line boundaries and no way to step between lines or reach the links on a line. Plain lines now carry `role="paragraph"`; lines that already contain a semantic block element, such as list items and headings from `ep_headings2`, keep their native semantics. The element itself is unchanged, so plugin selectors that target `div.ace-line` are unaffected. - **Plugins — `settings.ep_` config blocks are reachable again from `require()` (#8109, #8110).** Plugins read their own configuration out of a top-level `ep_*` block in `settings.json` via `require('ep_etherpad-lite/node/utils/Settings')`. The CJS-compatibility shim in `Settings.ts` installed accessor properties on `module.exports` for the keys present on the settings object *while that module was still evaluating* — but `ep_*` blocks are only merged in later, by the `reloadSettings()` call at the bottom of the same module. Every plugin config block was therefore invisible to the `require()` path (the value was reachable only under `.default`), so plugins silently fell back to their built-in defaults. For `ep_hash_auth` that meant `hash_dir` reverted to `/var/etherpad/users`, every hash lookup failed, and admin login returned 401 with no usable diagnostic — the symptom that surfaced this. The shim is now re-run after each settings load. Reported by @mathewcsims and @tris-ots; an equivalent fix was also proposed by @AkprasadoP in #8113. # 3.3.5 3.3.5 is a bug-fix follow-up to 3.3.4. It fixes a startup crash on fresh installs when pnpm 12 (now pnpm's default release) is installed, and makes the built-in updater work on Windows. ### Notable fixes - **Fresh installs no longer crash at startup under pnpm 12 (#8232).** On first boot (no `var/installed_plugins.json`), Etherpad runs `pnpm ls` to migrate plugins found in `node_modules`, passing `--no-production`. pnpm 12 renamed that flag to `--no-prod` and rejects the old name, so the server exited with `Error occurred while starting Etherpad` before it bound a port. The call now uses `--no-prod`, which pnpm 10 and 11 also accept. Existing installs were not affected. The repository, release tooling, Docker image and snap also move to pnpm 12. - **Updater — spawn pnpm through `cross-spawn` so updates work on Windows (#8218).** On Windows `pnpm` is a `.cmd` shim that plain `child_process.spawn` can't run, so the built-in update actions failed there. Thanks to Kaan Çelebi. # 3.3.4 3.3.4 is a security release. It closes a stored XSS in the `createDiffHTML` API output (GHSA-6vx2-3gwr-958v). ### Security - **Neutralize author IDs and colors in HTML diff export (GHSA-6vx2-3gwr-958v).** `getHTMLFromAtext` placed author colors inside a `