mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-19 01:24:19 +00:00
feat: make cookie names configurable with prefix setting (#7450)
* feat: make cookie names configurable with prefix setting Add cookie.prefix setting (default "ep_") that gets prepended to all cookie names set by Etherpad. This prevents conflicts with other applications on the same domain that use generic cookie names like "sessionID" or "token". Affected cookies: token, sessionID, language, prefs/prefsHttp, express_sid. The prefix is passed to the client via clientVars.cookiePrefix in the bootstrap templates so it's available before the handshake. Server-side cookie reads fall back to unprefixed names for backward compatibility during migration. Fixes #664 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: default cookie prefix to empty string for backward compatibility Changing the default to "ep_" would invalidate all existing sessions on upgrade since express-session only looks for the configured cookie name. Default to "" (no prefix) so upgrades are non-breaking — users opt-in to prefixed names by setting cookie.prefix in settings.json. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Qodo review — cookie prefix migration and fallbacks - l10n.ts: Read prefixed language cookie with fallback to unprefixed - welcome.ts: Use cookiePrefix for token transfer reads - timeslider.ts: Use prefix for sessionID in socket messages - pad_cookie.ts: Fall back to unprefixed prefs cookie for migration - indexBootstrap.js: Pass cookiePrefix via clientVars to welcome page - specialpages.ts: Pass settings to indexBootstrap template Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: escape regex metacharacters in cookie prefix, document Vite hardcode - l10n.ts: Escape special regex characters in cookiePrefix before using it in RegExp constructor to prevent runtime errors - padViteBootstrap.js: Add comment noting the hardcoded prefix is dev-only and must match settings.json Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * security: validate cookie prefix to prevent header injection Reject cookie.prefix values containing characters outside [a-zA-Z0-9_-] to prevent HTTP header injection via crafted cookie names (e.g., \r\n sequences). Falls back to empty prefix with an error log. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f0b84cc1d0
commit
474918a881
18 changed files with 68 additions and 18 deletions
|
|
@ -1058,6 +1058,7 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => {
|
|||
settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp,
|
||||
},
|
||||
initialChangesets: [], // FIXME: REMOVE THIS SHIT,
|
||||
cookiePrefix: settings.cookie.prefix,
|
||||
mode: process.env.NODE_ENV
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ exports.restartServer = async () => {
|
|||
saveUninitialized: false,
|
||||
// Set the cookie name to a javascript identifier compatible string. Makes code handling it
|
||||
// cleaner :)
|
||||
name: 'express_sid',
|
||||
name: `${settings.cookie.prefix}express_sid`,
|
||||
cookie: {
|
||||
maxAge: sessionLifetime || undefined, // Convert 0 to null.
|
||||
sameSite: settings.cookie.sameSite,
|
||||
|
|
|
|||
|
|
@ -76,8 +76,12 @@ exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Functio
|
|||
(async () => {
|
||||
// @ts-ignore
|
||||
const {session: {user} = {}} = req;
|
||||
const p = settings.cookie.prefix;
|
||||
const {accessStatus, authorID: authorId} = await securityManager.checkAccess(
|
||||
req.params.pad, req.cookies.sessionID, req.cookies.token, user);
|
||||
req.params.pad,
|
||||
req.cookies[`${p}sessionID`] || req.cookies.sessionID,
|
||||
req.cookies[`${p}token`] || req.cookies.token,
|
||||
user);
|
||||
if (accessStatus !== 'grant' || !webaccess.userCanModify(req.params.pad, req)) {
|
||||
return res.status(403).send('Forbidden');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -278,6 +278,7 @@ exports.expressCreateServer = async (_hookName: string, args: ArgsExpressType, c
|
|||
})
|
||||
|
||||
const indexString = eejs.require('ep_etherpad-lite/templates/indexBootstrap.js', {
|
||||
settings,
|
||||
})
|
||||
|
||||
const timeSliderString = eejs.require('ep_etherpad-lite/templates/timeSliderBootstrap.js', {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {ArgsExpressType} from "../../types/ArgsExpressType";
|
||||
const db = require('../../db/DB');
|
||||
import crypto from 'crypto'
|
||||
import settings from '../../utils/Settings';
|
||||
|
||||
|
||||
type TokenTransferRequest = {
|
||||
|
|
@ -38,8 +39,9 @@ export const expressCreateServer = (hookName:string, {app}:ArgsExpressType) =>
|
|||
|
||||
const token = await db.get(`${tokenTransferKey}:${id}`)
|
||||
|
||||
res.cookie('token', tokenData.token, {path: '/', maxAge: 1000*60*60*24*365});
|
||||
res.cookie('prefsHttp', tokenData.prefsHttp, {path: '/', maxAge: 1000*60*60*24*365});
|
||||
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);
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
'use strict';
|
||||
const securityManager = require('./db/SecurityManager');
|
||||
import settings from './utils/Settings';
|
||||
|
||||
// checks for padAccess
|
||||
module.exports = async (req: { params?: any; cookies?: any; session?: any; }, res: { status: (arg0: number) => { (): any; new(): any; send: { (arg0: string): void; new(): any; }; }; }) => {
|
||||
const {session: {user} = {}} = req;
|
||||
const p = settings.cookie.prefix;
|
||||
const accessObj = await securityManager.checkAccess(
|
||||
req.params.pad, req.cookies.sessionID, req.cookies.token, user);
|
||||
req.params.pad,
|
||||
req.cookies[`${p}sessionID`] || req.cookies.sessionID,
|
||||
req.cookies[`${p}token`] || req.cookies.token,
|
||||
user);
|
||||
|
||||
if (accessObj.accessStatus === 'grant') {
|
||||
// there is access, continue
|
||||
|
|
|
|||
|
|
@ -252,6 +252,7 @@ export type SettingsType = {
|
|||
trustProxy: boolean,
|
||||
cookie: {
|
||||
keyRotationInterval: number,
|
||||
prefix: string,
|
||||
sameSite: boolean | "lax" | "strict" | "none" | undefined,
|
||||
sessionLifetime: number,
|
||||
sessionRefreshInterval: number,
|
||||
|
|
@ -530,6 +531,7 @@ const settings: SettingsType = {
|
|||
*/
|
||||
cookie: {
|
||||
keyRotationInterval: 1 * 24 * 60 * 60 * 1000,
|
||||
prefix: '',
|
||||
sameSite: 'lax',
|
||||
sessionLifetime: 10 * 24 * 60 * 60 * 1000,
|
||||
sessionRefreshInterval: 1 * 24 * 60 * 60 * 1000,
|
||||
|
|
@ -1064,6 +1066,13 @@ export const reloadSettings = () => {
|
|||
'use automatic key rotation instead (see the cookie.keyRotationInterval setting).');
|
||||
}
|
||||
|
||||
// Validate cookie prefix to prevent header injection via cookie names
|
||||
if (settings.cookie.prefix && !/^[a-zA-Z0-9_-]*$/.test(settings.cookie.prefix)) {
|
||||
logger.error(`cookie.prefix "${settings.cookie.prefix}" contains invalid characters. ` +
|
||||
'Only alphanumeric characters, hyphens, and underscores are allowed. Using empty prefix.');
|
||||
settings.cookie.prefix = '';
|
||||
}
|
||||
|
||||
if (settings.dbType === 'dirty') {
|
||||
const dirtyWarning = 'DirtyDB is used. This is not recommended for production.';
|
||||
if (!settings.suppressErrorsInPadText) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue