mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-20 16:54:17 +00:00
* docs: PR3 GDPR anonymous identity hardening design spec
* docs: PR3 GDPR anon identity implementation plan
* feat(gdpr): ensureAuthorTokenCookie helper — HttpOnly server-set author token
* feat(gdpr): set HttpOnly author-token cookie from the pad routes
* feat(gdpr): read author token from cookie first, keep message.token fallback
* feat(gdpr): stop generating the author token client-side
* test(gdpr): server sets + reuses the HttpOnly author-token cookie
* fix+test(gdpr): parse token cookie from handshake Cookie header
socket.io handshake doesn't run cookie-parser, so socket.request.cookies
is undefined. Parse the Cookie header directly in handleClientReady so
the HttpOnly token actually resolves. Playwright spec covers HttpOnly
attribute, reload-stability, and context-isolation.
* docs(gdpr): token cookie is now HttpOnly + server-set
* fix(gdpr): close two HttpOnly token bypasses
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>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
68 lines
2.3 KiB
TypeScript
68 lines
2.3 KiB
TypeScript
import {ArgsExpressType} from "../../types/ArgsExpressType";
|
|
const db = require('../../db/DB');
|
|
import crypto from 'crypto'
|
|
import settings from '../../utils/Settings';
|
|
|
|
|
|
type TokenTransferRequest = {
|
|
token: string;
|
|
prefsHttp: string,
|
|
createdAt?: number;
|
|
}
|
|
|
|
const tokenTransferKey = "tokenTransfer:";
|
|
|
|
export const expressCreateServer = (hookName:string, {app}:ArgsExpressType) => {
|
|
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'});
|
|
}
|
|
|
|
const id = crypto.randomUUID();
|
|
const token: TokenTransferRequest = {
|
|
token: authorToken,
|
|
prefsHttp: body.prefsHttp || '',
|
|
createdAt: Date.now(),
|
|
};
|
|
|
|
await db.set(`${tokenTransferKey}:${id}`, token);
|
|
res.send({id});
|
|
})
|
|
|
|
app.get('/tokenTransfer/:token', async (req: any, res) => {
|
|
const id = req.params.token;
|
|
if (!id) {
|
|
return res.status(400).send({error: 'Invalid request'});
|
|
}
|
|
|
|
const tokenData = await db.get(`${tokenTransferKey}:${id}`);
|
|
if (!tokenData) {
|
|
return res.status(404).send({error: 'Token not found'});
|
|
}
|
|
|
|
const p = settings.cookie.prefix;
|
|
// 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);
|
|
})
|
|
}
|