Skip to content

feat(gdpr): HttpOnly author-token cookie (PR3 of #6701)#7548

Merged
JohnMcLear merged 10 commits intodevelopfrom
feat-gdpr-anon-identity
May 3, 2026
Merged

feat(gdpr): HttpOnly author-token cookie (PR3 of #6701)#7548
JohnMcLear merged 10 commits intodevelopfrom
feat-gdpr-anon-identity

Conversation

@JohnMcLear
Copy link
Copy Markdown
Member

Summary

  • Author-token cookie is now minted and set by the server on the pad route as HttpOnly; Secure (on HTTPS); SameSite=Lax (or None when cross-site embedded).
  • Browser JavaScript no longer reads, writes, or sends the token; the token field is dropped from the CLIENT_READY message.
  • handleClientReady parses the token from the socket.io handshake Cookie header (socket.io doesn't run cookie-parser). Legacy message.token is honoured for one release with a one-time WARN per session.
  • No IP-based identity fallback (already true; documented in cookies.md pointer to privacy.md — see PR2 feat(gdpr): IP/privacy audit (PR2 of #6701) #7547).

Part of the GDPR work tracked in #6701. PR1 #7546 landed deletion controls; PR2 #7547 landed the IP/privacy audit. Remaining PR4 (cookie banner) and PR5 (author erasure) stay in follow-ups.

Design spec: docs/superpowers/specs/2026-04-19-gdpr-pr3-anon-identity-design.md
Implementation plan: docs/superpowers/plans/2026-04-19-gdpr-pr3-anon-identity.md

Test plan

  • pnpm --filter ep_etherpad-lite run ts-check
  • ensureAuthorTokenCookie unit tests (5 cases: mint / reuse / Secure / SameSite=None when cross-site / invalid-existing)
  • authorTokenCookie integration tests (HttpOnly + reuse) — 2 passing
  • Playwright: HttpOnly attribute visible only via browser API, authorID stable across reload, authorID differs in isolated second context — 3 passing
  • Regression: chat + enter specs pass unchanged

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented Apr 19, 2026

Review Summary by Qodo

(Agentic_describe updated until commit 89fde85)

Move author-token cookie to server-set HttpOnly (GDPR PR3)

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Server now mints and sets author-token as HttpOnly cookie on pad routes
• Browser JavaScript no longer reads, writes, or generates the token
• Socket.io handshake reads token from cookie; legacy message.token supported with deprecation
  warning
• Comprehensive backend and frontend tests validate cookie lifecycle and identity stability
Diagram
flowchart LR
  A["Browser GET /p/:pad"] -->|ensureAuthorTokenCookie| B["Server mints t.* token"]
  B -->|Set-Cookie HttpOnly| C["Browser receives cookie"]
  C -->|socket.io handshake| D["handleClientReady reads from socket.request.cookies"]
  D -->|resolves token| E["Author identity established"]
  F["Legacy client sends message.token"] -->|fallback path| D
  D -->|warn once| G["Deprecation logged"]
Loading

Grey Divider

File Changes

1. src/node/utils/ensureAuthorTokenCookie.ts ✨ Enhancement +35/-0

New helper to mint and set HttpOnly author token

src/node/utils/ensureAuthorTokenCookie.ts


2. src/node/hooks/express/specialpages.ts ✨ Enhancement +5/-0

Call ensureAuthorTokenCookie on pad and timeslider routes

src/node/hooks/express/specialpages.ts


3. src/node/handler/PadMessageHandler.ts ✨ Enhancement +22/-1

Read token from socket cookie, fallback to message.token

src/node/handler/PadMessageHandler.ts


View more (10)
4. src/static/js/pad.ts ✨ Enhancement +3/-6

Remove client-side token generation and cookie write

src/static/js/pad.ts


5. src/static/js/timeslider.ts ✨ Enhancement +5/-9

Remove token read/write and message field from timeslider

src/static/js/timeslider.ts


6. src/node/hooks/express/tokenTransfer.ts 🐞 Bug fix +34/-13

Read token from request cookies, set as HttpOnly on transfer

src/node/hooks/express/tokenTransfer.ts


7. src/static/js/welcome.ts ✨ Enhancement +3/-1

Stop sending token in session transfer request body

src/static/js/welcome.ts


8. src/tests/backend/specs/ensureAuthorTokenCookie.ts 🧪 Tests +77/-0

Unit tests for ensureAuthorTokenCookie helper function

src/tests/backend/specs/ensureAuthorTokenCookie.ts


9. src/tests/backend/specs/authorTokenCookie.ts 🧪 Tests +47/-0

Integration tests for HttpOnly token cookie lifecycle

src/tests/backend/specs/authorTokenCookie.ts


10. src/tests/frontend-new/specs/author_token_cookie.spec.ts 🧪 Tests +49/-0

Playwright tests for HttpOnly attribute and identity stability

src/tests/frontend-new/specs/author_token_cookie.spec.ts


11. doc/cookies.md 📝 Documentation +1/-1

Update token cookie documentation to reflect HttpOnly server-set

doc/cookies.md


12. docs/superpowers/specs/2026-04-19-gdpr-pr3-anon-identity-design.md 📝 Documentation +181/-0

Design specification for GDPR PR3 anonymous identity hardening

docs/superpowers/specs/2026-04-19-gdpr-pr3-anon-identity-design.md


13. docs/superpowers/plans/2026-04-19-gdpr-pr3-anon-identity.md 📝 Documentation +587/-0

Implementation plan with step-by-step task breakdown

docs/superpowers/plans/2026-04-19-gdpr-pr3-anon-identity.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented Apr 19, 2026

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (3) 📎 Requirement gaps (0)

Grey Divider


Action required

1. /tokenTransfer ignores body.token 📘 Rule violation ☼ Reliability
Description
The /tokenTransfer POST handler now rejects requests without an author-token cookie and no longer
accepts token from the request body, which can break existing clients calling this endpoint. The
removed behavior is not deprecated with WARN logging.
Code

src/node/hooks/express/tokenTransfer.ts[R16-27]

+  app.post('/tokenTransfer', async (req: any, res) => {
+    // The author token is HttpOnly (ether/etherpad#6701 PR3) so the browser
+    // cannot read it. Read it off the request's own cookie jar instead of
+    // trusting the request body. The client still supplies non-HttpOnly
+    // prefs via body because `prefsHttp` is intentionally JS-readable.
+    const cp = settings.cookie.prefix || '';
+    const authorToken: string | undefined =
+        req.cookies?.[`${cp}token`] || req.cookies?.token;
+    const body = (req.body || {}) as Partial<TokenTransferRequest>;
+    if (!authorToken) {
+      return res.status(400).send({error: 'No author cookie to transfer'});
   }
Evidence
PR Compliance ID 3 requires avoiding breaking API changes without compatibility measures, and ID 4
requires WARN-based deprecation instead of outright removal. The new handler only reads the author
token from req.cookies and returns 400 if it is missing, with no fallback to the legacy body
token and no WARN indicating deprecation/removal.

src/node/hooks/express/tokenTransfer.ts[16-27]
Best Practice: Repository guidelines
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`POST /tokenTransfer` no longer accepts the author token via `body.token` and hard-requires a cookie (`req.cookies[<prefix>token]`), returning `400` otherwise. This is a breaking change for any existing callers and removes previous behavior without a WARN-based deprecation window.
## Issue Context
For one release, keep backwards compatibility similarly to the CLIENT_READY `message.token` approach: accept `body.token` as a fallback when the cookie is absent, validate it, and log a WARN indicating deprecation and timeline.
## Fix Focus Areas
- src/node/hooks/express/tokenTransfer.ts[16-34]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Missing token creates shared author 🐞 Bug ⛨ Security
Description
PadMessageHandler can persist a null token into thisSession.auth.token (cookie
missing/unparseable and client no longer sends message.token), and SecurityManager.checkAccess()
does not deny missing tokens, so AuthorManager.getAuthorId() will map the literal DB key
token2author:null and grant access under a shared authorID. This can cause unintended cross-user
author identity collisions and can allow tokenless socket connections to be treated as authenticated
for public pads.
Code

src/node/handler/PadMessageHandler.ts[R300-322]

+    const cookiePrefix = settings.cookie?.prefix || '';
+    const cookieHeader: string = socket.request?.headers?.cookie || '';
+    const cookieName = `${cookiePrefix}token`;
+    const cookieMatch = cookieHeader.split(/;\s*/).find(
+        (c) => c.split('=')[0] === cookieName);
+    const cookieToken = cookieMatch ? decodeURIComponent(cookieMatch.split('=').slice(1).join('=')) : null;
+    const legacyToken = typeof message.token === 'string' ? message.token : null;
+    const resolvedToken = cookieToken || legacyToken;
+    if (!cookieToken && legacyToken && !thisSession.legacyTokenWarned) {
+      messageLogger.warn(
+          'client sent author token via CLIENT_READY message; cookie migration ' +
+          'will take effect on next HTTP response. ' +
+          'See docs/superpowers/specs/2026-04-19-gdpr-pr3-anon-identity-design.md');
+      thisSession.legacyTokenWarned = true;
+    }
   // Remember this information since we won't have the cookie in further socket.io messages. This
   // information will be used to check if the sessionId of this connection is still valid since it
   // could have been deleted by the API.
   thisSession.auth = {
     sessionID: message.sessionID,
     padID: message.padId,
-      token: message.token,
+      token: resolvedToken,
   };
Evidence
The new CLIENT_READY path sets resolvedToken = cookieToken || legacyToken and stores it without
requiring a non-null string; new clients no longer send token in CLIENT_READY so legacyToken
will often be null; downstream, SecurityManager.checkAccess() only validates tokens when `token !=
null and then passes the (possibly null) token to AuthorManager.getAuthorId()`, which uses it
directly as part of the DB key, creating a stable mapping for null.

src/node/handler/PadMessageHandler.ts[293-322]
src/static/js/pad.ts[179-212]
src/node/db/SecurityManager.ts[112-126]
src/node/db/AuthorManager.ts[117-127]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Socket authentication can proceed with `token === null`, which then gets used as a DB key (`token2author:null`) and results in shared/incorrect author identity.
### Issue Context
New clients no longer send `message.token`, so correctness depends on successfully extracting a valid token from the socket.io handshake Cookie header. If the cookie is missing or parsing fails, the current code stores `null` and downstream code does not reject it.
### Fix Focus Areas
- src/node/handler/PadMessageHandler.ts[293-322]
- src/node/db/SecurityManager.ts[112-126]
- src/node/db/AuthorManager.ts[117-127]
- src/static/js/pad.ts[179-212]
### Suggested fix
- In `PadMessageHandler`, after resolving the token, require `typeof resolvedToken === 'string' && padutils.isValidAuthorToken(resolvedToken)` (or equivalent) when there is no valid HTTP API session; otherwise emit `accessStatus: 'deny'` / disconnect with a clear error.
- Defense-in-depth: in `SecurityManager.checkAccess()`, add an explicit deny when `!sessionAuthorID && (token == null || token === '')`.
- Consider using a robust cookie parser and trimming cookie names/values so legitimate cookies don’t accidentally parse as missing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. TokenTransfer returns plaintext token🐞 Bug ⛨ Security
Description
GET /tokenTransfer/:token responds with tokenData (including the plaintext author token) and
does not invalidate the transfer record, so any script running in-origin can mint an id and
exfiltrate a long-lived author token despite HttpOnly cookies. The id is also reusable indefinitely
because the DB entry is never deleted.
Code

src/node/hooks/express/tokenTransfer.ts[R48-67]

     return res.status(404).send({error: 'Token not found'});
   }

-    const token = await db.get(`${tokenTransferKey}:${id}`)
-
   const p = settings.cookie.prefix;
-    res.cookie(`${p}token`, tokenData.token, {path: '/', maxAge: 1000*60*60*24*365});
-    res.cookie(`${p}prefsHttp`, tokenData.prefsHttp, {path: '/', maxAge: 1000*60*60*24*365});
-    res.send(token);
+    // Re-issue the author token on the new device as an HttpOnly cookie to
+    // match the /p/:pad path (ether/etherpad#6701 PR3). Without this, the
+    // transfer would reintroduce a JS-readable copy of the token.
+    res.cookie(`${p}token`, tokenData.token, {
+      path: '/',
+      maxAge: 1000 * 60 * 60 * 24 * 365,
+      httpOnly: true,
+      secure: Boolean(req.secure),
+      sameSite: 'lax',
+    });
+    // prefsHttp is intentionally JS-readable — do NOT mark HttpOnly.
+    res.cookie(`${p}prefsHttp`, tokenData.prefsHttp, {
+      path: '/', maxAge: 1000 * 60 * 60 * 24 * 365,
+    });
+    res.send(tokenData);
 })
Evidence
The handler reads tokenData from the DB and then res.send(tokenData), which includes
tokenData.token; there is no delete/expiry call after successful retrieval, making the transfer
capability a persistent bearer mechanism.

src/node/hooks/express/tokenTransfer.ts[46-67]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The token transfer flow still exposes the plaintext author token in an HTTP response body and leaves transfer IDs reusable.
### Issue Context
HttpOnly prevents reading `document.cookie`, but it does not help if an endpoint returns the token via JSON.
### Fix Focus Areas
- src/node/hooks/express/tokenTransfer.ts[40-67]
### Suggested fix
- After a successful GET, delete the DB entry (`tokenTransferKey:${id}`) so the id is one-time.
- Do not include `token` in the JSON response body; instead, set cookies and return a minimal `{ok:true}` (or redirect to a safe page).
- Add a short TTL/expiry check using `createdAt` and reject/cleanup old transfer records.
- Validate `tokenData.token` with `padutils.isValidAuthorToken()` before re-issuing it as a cookie.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (3)
4. No flag for HttpOnly token 📘 Rule violation ☼ Reliability
Description
The new server-minted HttpOnly author-token cookie behavior is enabled unconditionally on pad
routes, with no feature flag or default-off toggle. This violates the requirement that new features
be gated to reduce rollout risk and allow safe disablement.
Code

src/node/hooks/express/specialpages.ts[191]

+        ensureAuthorTokenCookie(req, res, settings);
Evidence
PR Compliance ID 5 requires new features to be behind a feature flag and disabled by default. The
pad routes now always call ensureAuthorTokenCookie(req, res, settings) (no conditional), making
the new behavior always-on.

src/node/hooks/express/specialpages.ts[191-191]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new HttpOnly author-token cookie behavior is always enabled, but PR compliance requires new features to be behind a feature flag and disabled by default.
## Issue Context
`ensureAuthorTokenCookie()` is now called on all pad and timeslider routes with no conditional gating, changing behavior immediately for all deployments.
## Fix Focus Areas
- src/node/hooks/express/specialpages.ts[191-191]
- src/node/hooks/express/specialpages.ts[222-222]
- src/node/hooks/express/specialpages.ts[354-354]
- src/node/hooks/express/specialpages.ts[375-375]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Timeslider rewrites token cookie🐞 Bug ⛨ Security
Description
The timeslider client still reads and (re)sets the author token cookie from JavaScript and sends it
over socket messages, which can overwrite the new server-minted HttpOnly token cookie and keep the
legacy message.token path active. This defeats the primary security goal (token not
JS-readable/writable) for any user who opens the timeslider.
Code

src/node/hooks/express/specialpages.ts[R221-223]

   setRouteHandler("/p/:pad/timeslider", (req: any, res: any, next: Function) => {
+        ensureAuthorTokenCookie(req, res, settings);
     console.log("Reloading pad")
Evidence
The server now ensures the author token cookie is set on the timeslider route via
ensureAuthorTokenCookie(). However, the existing timeslider frontend still calls
Cookies.get()/Cookies.set() for ${cp}token and includes token in its socket message payload,
which is incompatible with an HttpOnly cookie and can downgrade/replace it with a JS-managed cookie
value.

src/node/hooks/express/specialpages.ts[221-223]
src/node/utils/ensureAuthorTokenCookie.ts[17-34]
src/static/js/timeslider.ts[52-58]
src/static/js/timeslider.ts[100-108]
src/static/js/pad.ts[190-193]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The timeslider frontend still generates/sets the author token cookie in JavaScript and sends `token` in socket messages, which undermines the new HttpOnly cookie model.
### Issue Context
The server now sets the author token as an HttpOnly cookie on `/p/:pad/timeslider`, but the timeslider client still treats the token as JS-managed state.
### Fix Focus Areas
- src/static/js/timeslider.ts[52-58]
- src/static/js/timeslider.ts[100-109]
- src/static/js/pad.ts[190-212]
### What to change
- Remove the `token = Cookies.get(...); if (token == null) Cookies.set(...);` block.
- Remove `token` from the socket message payload (similar to `src/static/js/pad.ts` removing `token` from CLIENT_READY).
- Ensure the server path works purely via the socket.io handshake cookie (no message.token dependence for timeslider clients).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. tokenTransfer breaks HttpOnly model🐞 Bug ⛨ Security
Description
The welcome page’s tokenTransfer POST reads the author token from document.cookie (now invisible due
to HttpOnly), causing invalid requests, and the tokenTransfer GET endpoint re-sets the token cookie
without HttpOnly, undoing the hardening. This both breaks the feature and reintroduces a JS-readable
author token cookie.
Code

src/node/utils/ensureAuthorTokenCookie.ts[R26-33]

+  const token = padutils.generateAuthorToken();
+  res.cookie(cookieName, token, {
+    httpOnly: true,
+    secure: Boolean(req.secure),
+    sameSite: isCrossSiteEmbed(req) ? 'none' : 'lax',
+    maxAge: 60 * 24 * 60 * 60 * 1000, // 60 days — matches the pre-PR3 client default
+    path: '/',
+  });
Evidence
ensureAuthorTokenCookie sets the author token cookie with httpOnly:true, so welcome.ts cannot read
it from document.cookie to POST /tokenTransfer. Additionally, tokenTransfer.ts sets ${p}token via
res.cookie without httpOnly/sameSite/secure, which will create a non-HttpOnly author token cookie
and bypass the intended protection.

src/node/utils/ensureAuthorTokenCookie.ts[26-33]
src/static/js/welcome.ts[24-33]
src/node/hooks/express/tokenTransfer.ts[16-20]
src/node/hooks/express/tokenTransfer.ts[42-45]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The tokenTransfer flow is incompatible with an HttpOnly author token: it tries to read the token in the browser and it also writes a non-HttpOnly token cookie server-side.
### Issue Context
The author token is now intended to be server-minted and HttpOnly. Any feature that depends on reading/writing the token in browser JS will break or regress security.
### Fix Focus Areas
- src/static/js/welcome.ts[24-33]
- src/node/hooks/express/tokenTransfer.ts[16-20]
- src/node/hooks/express/tokenTransfer.ts[42-45]
- src/node/utils/ensureAuthorTokenCookie.ts[26-33]
### What to change
- Update `POST /tokenTransfer` to not require `token` from the request body. Instead, read the token from `req.cookies` (include prefixed + unprefixed fallback).
- When setting `${p}token` in `GET /tokenTransfer/:token`, set cookie options consistent with PR3 (httpOnly, sameSite, secure-on-https, path='/').
- Update `welcome.ts` to stop attempting to read `${cp}token` from `document.cookie` and stop sending it in the POST body.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

7. Existing token stays readable 🐞 Bug ⛨ Security
Description
ensureAuthorTokenCookie() returns early when a valid token cookie is present and does not emit
Set-Cookie, so pre-PR3 JS-readable token cookies will not get upgraded to HttpOnly until they
expire. This reduces the effective security improvement for returning users.
Code

src/node/utils/ensureAuthorTokenCookie.ts[R20-25]

+  const prefix = settings.cookie?.prefix || '';
+  const cookieName = `${prefix}token`;
+  const existing = req.cookies?.[cookieName];
+  if (typeof existing === 'string' && padutils.isValidAuthorToken(existing)) {
+    return existing;
+  }
Evidence
The helper is explicitly idempotent: it does not call res.cookie() when req.cookies[cookieName]
is a valid token, so existing cookies keep their old attributes because attributes can only be
changed via a new Set-Cookie response.

src/node/utils/ensureAuthorTokenCookie.ts[20-34]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Existing valid token cookies are not re-issued, so they retain their pre-PR3 (non-HttpOnly) attributes.
### Issue Context
Browsers do not send cookie attributes back to the server; the only way to change attributes is to send `Set-Cookie`.
### Fix Focus Areas
- src/node/utils/ensureAuthorTokenCookie.ts[20-34]
### Suggested fix
- Consider re-issuing the token cookie (same value) at least once per browser (or per express-session) to apply the new attributes.
- One approach: if a valid cookie is present, still call `res.cookie(cookieName, existing, {httpOnly:true,...})` but guard it with a session flag to avoid sending it on every request.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. CLIENT_READY no longer sends token 📘 Rule violation ☼ Reliability
Description
The browser no longer includes token in the CLIENT_READY message, which can break
interoperability with older servers that still rely on message.token. This is a
backward-compatibility risk for the public client/server interface during mixed-version or
cached-asset scenarios.
Code

src/static/js/pad.ts[212]

-    token,
Evidence
PR Compliance ID 3 requires maintaining backward compatibility for public interfaces. The PR removes
the token field from the outgoing CLIENT_READY payload (client-side), while the server
previously read message.token for authentication state, indicating a protocol expectation in older
versions.

src/static/js/pad.ts[212-212]
src/node/handler/PadMessageHandler.ts[300-300]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The client no longer sends `token` in `CLIENT_READY`, which can break compatibility with older servers that still require `message.token`.
## Issue Context
This is a client/server protocol surface; mixed-version scenarios (cached client assets, staggered deployments) can cause new-client/old-server failures.
## Fix Focus Areas
- src/static/js/pad.ts[207-214]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Missing unprefixed token fallback 🐞 Bug ≡ Correctness
Description
The new cookie lookup logic only checks ${prefix}token, so installations that previously relied on
an unprefixed token cookie (or that changed cookie.prefix) will silently mint a new token and
change the anonymous author identity. This is inconsistent with other server paths that still fall
back to unprefixed cookies.
Code

src/node/utils/ensureAuthorTokenCookie.ts[R20-24]

+  const prefix = settings.cookie?.prefix || '';
+  const cookieName = `${prefix}token`;
+  const existing = req.cookies?.[cookieName];
+  if (typeof existing === 'string' && padutils.isValidAuthorToken(existing)) {
+    return existing;
Evidence
ensureAuthorTokenCookie only checks req.cookies[cookieName] (prefixed name), and
PadMessageHandler’s handshake parsing also targets only the prefixed name. But other server code
(padaccess) explicitly falls back to unprefixed req.cookies.token, indicating compatibility
expectations that this PR’s new path doesn’t preserve.

src/node/utils/ensureAuthorTokenCookie.ts[20-24]
src/node/handler/PadMessageHandler.ts[300-306]
src/node/padaccess.ts[8-13]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Author token resolution in PR3 does not fall back to the unprefixed `token` cookie when `settings.cookie.prefix` is set, which can reset identity for existing users.
### Issue Context
Other code paths already use `${prefix}token || token` fallback.
### Fix Focus Areas
- src/node/utils/ensureAuthorTokenCookie.ts[20-24]
- src/node/handler/PadMessageHandler.ts[300-307]
- src/node/padaccess.ts[8-13]
### What to change
- In `ensureAuthorTokenCookie`, if `cookieName` is not `'token'`, also check `req.cookies.token` as a fallback.
- If the fallback is used and valid, consider setting the prefixed cookie (HttpOnly) to the same value to migrate.
- In `PadMessageHandler`, if the prefixed cookie is absent, also try parsing `token=` from the handshake Cookie header (legacy/migration support).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
10. Invalid SameSite=None on HTTP 🐞 Bug ☼ Reliability
Description
ensureAuthorTokenCookie can set SameSite=None when Sec-Fetch-Site=cross-site even if req.secure is
false, producing a cookie configuration that modern browsers reject and drop. This can break
embedded/cross-site usage on non-HTTPS or misconfigured TLS termination.
Code

src/node/utils/ensureAuthorTokenCookie.ts[R27-32]

+  res.cookie(cookieName, token, {
+    httpOnly: true,
+    secure: Boolean(req.secure),
+    sameSite: isCrossSiteEmbed(req) ? 'none' : 'lax',
+    maxAge: 60 * 24 * 60 * 60 * 1000, // 60 days — matches the pre-PR3 client default
+    path: '/',
Evidence
The helper sets sameSite: 'none' solely based on sec-fetch-site while secure is tied to
req.secure. The shared cookie code documents that SameSite=None does not work unless Secure is
true, so the helper can emit a cookie that will be ignored by browsers.

src/node/utils/ensureAuthorTokenCookie.ts[27-32]
src/static/js/pad_utils.ts[506-516]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The server may emit `SameSite=None` without `Secure`, which browsers reject.
### Issue Context
Cross-site embeds require `SameSite=None`, but only when `Secure=true`.
### Fix Focus Areas
- src/node/utils/ensureAuthorTokenCookie.ts[5-33]
- src/tests/backend/specs/ensureAuthorTokenCookie.ts[53-63]
### What to change
- Only set `sameSite: 'none'` if `req.secure` is true; otherwise fall back to `'lax'`.
- Add/adjust a unit test for the `cross-site + insecure` case to assert it does **not** emit `SameSite=None` without Secure.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Handshake cookie decode can throw 🐞 Bug ☼ Reliability
Description
PadMessageHandler uses decodeURIComponent() on the raw cookie value without guarding against
malformed percent-encoding, which can throw and fail the CLIENT_READY processing for that socket. A
single bad Cookie header can reliably trigger errors and disconnects.
Code

src/node/handler/PadMessageHandler.ts[R301-306]

+    const cookieHeader: string = socket.request?.headers?.cookie || '';
+    const cookieName = `${cookiePrefix}token`;
+    const cookieMatch = cookieHeader.split(/;\s*/).find(
+        (c) => c.split('=')[0] === cookieName);
+    const cookieToken = cookieMatch ? decodeURIComponent(cookieMatch.split('=').slice(1).join('=')) : null;
+    const legacyToken = typeof message.token === 'string' ? message.token : null;
Evidence
The code unconditionally calls decodeURIComponent() if it finds a matching cookie name.
decodeURIComponent throws on invalid percent-encoding, and SocketIORouter treats thrown errors as
message handling failures for that connection.

src/node/handler/PadMessageHandler.ts[301-306]
src/node/handler/SocketIORouter.ts[79-91]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Malformed cookie values can cause decodeURIComponent() to throw and break CLIENT_READY handling.
### Issue Context
Cookie values for the author token are expected to be base64url and do not need percent-decoding in the normal case.
### Fix Focus Areas
- src/node/handler/PadMessageHandler.ts[301-307]
### What to change
- Avoid decodeURIComponent entirely (treat the cookie value as opaque), or
- Wrap decodeURIComponent in try/catch and treat failures as `null` (optionally log at debug).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

12. CLIENT_READY types still require token 🐞 Bug ⚙ Maintainability
Description
The SocketIO message type definitions still require token in CLIENT_READY even though pad.ts no
longer sends it, making the type contracts misleading for future changes and refactors. This can
cause incorrect assumptions elsewhere (especially around timeslider / other clients).
Code

src/static/js/pad.ts[R207-211]

 type: 'CLIENT_READY',
 padId,
 sessionID: Cookies.get(`${cp}sessionID`) || Cookies.get('sessionID'),
-    token,
 userInfo,
};
Evidence
pad.ts now constructs CLIENT_READY without a token field, but the shared type definition still
defines ClientReadyMessage.token: string, implying all CLIENT_READY messages must include token.

src/static/js/pad.ts[205-211]
src/static/js/types/SocketIOMessage.ts[180-187]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Type definitions still model CLIENT_READY as always including `token`.
### Issue Context
PR3 removes `token` from the main pad client; other clients should converge on the handshake-cookie model.
### Fix Focus Areas
- src/static/js/types/SocketIOMessage.ts[180-189]
- src/static/js/pad.ts[205-211]
### What to change
- Make `token` optional (or remove it) in the CLIENT_READY message type(s).
- If legacy token support remains for one release, model it as `token?: string` and document deprecation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo



setRouteHandler("/p/:pad", (req: any, res: any, next: Function) => {
ensureAuthorTokenCookie(req, res, settings);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. No flag for httponly token 📘 Rule violation ☼ Reliability

The new server-minted HttpOnly author-token cookie behavior is enabled unconditionally on pad
routes, with no feature flag or default-off toggle. This violates the requirement that new features
be gated to reduce rollout risk and allow safe disablement.
Agent Prompt
## Issue description
The new HttpOnly author-token cookie behavior is always enabled, but PR compliance requires new features to be behind a feature flag and disabled by default.

## Issue Context
`ensureAuthorTokenCookie()` is now called on all pad and timeslider routes with no conditional gating, changing behavior immediately for all deployments.

## Fix Focus Areas
- src/node/hooks/express/specialpages.ts[191-191]
- src/node/hooks/express/specialpages.ts[222-222]
- src/node/hooks/express/specialpages.ts[354-354]
- src/node/hooks/express/specialpages.ts[375-375]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/node/hooks/express/specialpages.ts
Comment thread src/node/utils/ensureAuthorTokenCookie.ts
Qodo review:
- Timeslider still ran the pre-PR3 JS-cookie path: it read
  Cookies.get('${cp}token') (which HttpOnly hides), then generated a
  fresh plaintext token and overwrote the server's HttpOnly cookie with
  it, and sent token in every socket message. Strip the token read/
  write entirely from timeslider.ts and from the outgoing message
  shape; the server reads the cookie off the socket.io handshake just
  like on /p/:pad.
- tokenTransfer re-issued the author cookie without HttpOnly, undoing
  the hardening the first time a user transferred a session. Re-set
  it as HttpOnly + Secure (on HTTPS) + SameSite=Lax. Also stop
  trusting the body-supplied token on POST: read it off req.cookies
  server-side so the client never needs JS access to the token.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JohnMcLear JohnMcLear marked this pull request as draft April 27, 2026 09:55
@JohnMcLear JohnMcLear marked this pull request as ready for review May 3, 2026 04:49
@qodo-code-review
Copy link
Copy Markdown

ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one.

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 3, 2026

Persistent review updated to latest commit 89fde85

Comment on lines +16 to 27
app.post('/tokenTransfer', async (req: any, res) => {
// The author token is HttpOnly (ether/etherpad#6701 PR3) so the browser
// cannot read it. Read it off the request's own cookie jar instead of
// trusting the request body. The client still supplies non-HttpOnly
// prefs via body because `prefsHttp` is intentionally JS-readable.
const cp = settings.cookie.prefix || '';
const authorToken: string | undefined =
req.cookies?.[`${cp}token`] || req.cookies?.token;
const body = (req.body || {}) as Partial<TokenTransferRequest>;
if (!authorToken) {
return res.status(400).send({error: 'No author cookie to transfer'});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. /tokentransfer ignores body.token 📘 Rule violation ☼ Reliability

The /tokenTransfer POST handler now rejects requests without an author-token cookie and no longer
accepts token from the request body, which can break existing clients calling this endpoint. The
removed behavior is not deprecated with WARN logging.
Agent Prompt
## Issue description
`POST /tokenTransfer` no longer accepts the author token via `body.token` and hard-requires a cookie (`req.cookies[<prefix>token]`), returning `400` otherwise. This is a breaking change for any existing callers and removes previous behavior without a WARN-based deprecation window.

## Issue Context
For one release, keep backwards compatibility similarly to the CLIENT_READY `message.token` approach: accept `body.token` as a fallback when the cookie is absent, validate it, and log a WARN indicating deprecation and timeline.

## Fix Focus Areas
- src/node/hooks/express/tokenTransfer.ts[16-34]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +300 to 322
const cookiePrefix = settings.cookie?.prefix || '';
const cookieHeader: string = socket.request?.headers?.cookie || '';
const cookieName = `${cookiePrefix}token`;
const cookieMatch = cookieHeader.split(/;\s*/).find(
(c) => c.split('=')[0] === cookieName);
const cookieToken = cookieMatch ? decodeURIComponent(cookieMatch.split('=').slice(1).join('=')) : null;
const legacyToken = typeof message.token === 'string' ? message.token : null;
const resolvedToken = cookieToken || legacyToken;
if (!cookieToken && legacyToken && !thisSession.legacyTokenWarned) {
messageLogger.warn(
'client sent author token via CLIENT_READY message; cookie migration ' +
'will take effect on next HTTP response. ' +
'See docs/superpowers/specs/2026-04-19-gdpr-pr3-anon-identity-design.md');
thisSession.legacyTokenWarned = true;
}
// Remember this information since we won't have the cookie in further socket.io messages. This
// information will be used to check if the sessionId of this connection is still valid since it
// could have been deleted by the API.
thisSession.auth = {
sessionID: message.sessionID,
padID: message.padId,
token: message.token,
token: resolvedToken,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Missing token creates shared author 🐞 Bug ⛨ Security

PadMessageHandler can persist a null token into thisSession.auth.token (cookie
missing/unparseable and client no longer sends message.token), and SecurityManager.checkAccess()
does not deny missing tokens, so AuthorManager.getAuthorId() will map the literal DB key
token2author:null and grant access under a shared authorID. This can cause unintended cross-user
author identity collisions and can allow tokenless socket connections to be treated as authenticated
for public pads.
Agent Prompt
### Issue description
Socket authentication can proceed with `token === null`, which then gets used as a DB key (`token2author:null`) and results in shared/incorrect author identity.

### Issue Context
New clients no longer send `message.token`, so correctness depends on successfully extracting a valid token from the socket.io handshake Cookie header. If the cookie is missing or parsing fails, the current code stores `null` and downstream code does not reject it.

### Fix Focus Areas
- src/node/handler/PadMessageHandler.ts[293-322]
- src/node/db/SecurityManager.ts[112-126]
- src/node/db/AuthorManager.ts[117-127]
- src/static/js/pad.ts[179-212]

### Suggested fix
- In `PadMessageHandler`, after resolving the token, require `typeof resolvedToken === 'string' && padutils.isValidAuthorToken(resolvedToken)` (or equivalent) when there is no valid HTTP API session; otherwise emit `accessStatus: 'deny'` / disconnect with a clear error.
- Defense-in-depth: in `SecurityManager.checkAccess()`, add an explicit deny when `!sessionAuthorID && (token == null || token === '')`.
- Consider using a robust cookie parser and trimming cookie names/values so legitimate cookies don’t accidentally parse as missing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/node/hooks/express/tokenTransfer.ts
@JohnMcLear JohnMcLear merged commit 49bc33f into develop May 3, 2026
36 checks passed
@JohnMcLear JohnMcLear deleted the feat-gdpr-anon-identity branch May 3, 2026 04:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants