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 asettings.usersaccount with nopasswordproperty compared against the literal string"undefined", and one with"password": nullagainst"null". Submitting that literal logged the account in and issued a token carrying itsadminclaim; with the defaultauthenticationMethod: "sso"the HTTP API accepts that token. Accounts without a usable password occur in practice — the container image leavespasswordnull whenADMIN_PASSWORDis unset, and anep_hash_authentry replacespasswordwithhash. The sibling HTTP Basic path inwebaccess.tsalready failed closed here; the credential check now lives inverifyInteractiveLogin()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 theauthenticatehook, which it does not consult. Reported by Wenhao Wu (Southeast University).
Notable fixes
- PDF export honours
font-familywithout LibreOffice (#8245, #8249). The built-in PDF path used only pdfkit's Helvetica and Courier and ignoredfont-familyentirely, so any font applied by a plugin such asep_font_familywas 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 newexportPdfFontssetting, 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.ps1read the major version withnode -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 receivedsplit(.)[0]and threw aSyntaxError. The empty result became 0, and the installer rejected every Node version as too old. The version now comes fromnode --versionparsed 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
defaultPadTextwere 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\nwas 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 typingWelcome\n\ntest\nwrites the same bytes as editingsettings.jsonby 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
prefsHttpcookie, but over HTTPS the pad stores its preferences inprefs, so nothing was sent and the receiving side wrote a cookie the destination never reads. The client also sent the cookie still percent-encoded andres.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 writesprefsorprefsHttpaccording toreq.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
<div>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 carryrole="paragraph"; lines that already contain a semantic block element, such as list items and headings fromep_headings2, keep their native semantics. The element itself is unchanged, so plugin selectors that targetdiv.ace-lineare unaffected. - Plugins —
settings.ep_<plugin>config blocks are reachable again fromrequire()(#8109, #8110). Plugins read their own configuration out of a top-levelep_*block insettings.jsonviarequire('ep_etherpad-lite/node/utils/Settings'). The CJS-compatibility shim inSettings.tsinstalled accessor properties onmodule.exportsfor the keys present on the settings object while that module was still evaluating — butep_*blocks are only merged in later, by thereloadSettings()call at the bottom of the same module. Every plugin config block was therefore invisible to therequire()path (the value was reachable only under.default), so plugins silently fell back to their built-in defaults. Forep_hash_auththat meanthash_dirreverted 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.