From 9ee12a84da43ab12d03ae562ae2d693e8f6cdad8 Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:12:28 +0330 Subject: [PATCH] Import source: backend/src/middleware/session Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- backend/src/middleware/session.ts | 86 +++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 backend/src/middleware/session.ts diff --git a/backend/src/middleware/session.ts b/backend/src/middleware/session.ts new file mode 100644 index 0000000..1025087 --- /dev/null +++ b/backend/src/middleware/session.ts @@ -0,0 +1,86 @@ +import { randomBytes } from 'node:crypto'; +import type { Request, RequestHandler } from 'express'; +import { sha256 } from '../utils/crypto.js'; +import { env } from '../config/env.js'; +import { store } from '../services/store.js'; +import type { DatabaseShape } from '../types.js'; + +export const SESSION_COOKIE = 'cshare_sid'; +const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000; +const ROTATION_INTERVAL_MS = 24 * 60 * 60 * 1000; // Rotate session once a day + +// Map of sessionId -> { clientHash: string, expiresAt: number } +const sessionCache = new Map(); + +function getClientFingerprint(req: Request): string { + const ip = req.ip ?? req.socket.remoteAddress ?? 'unknown'; + const ua = req.headers['user-agent'] as string ?? 'unknown'; + return sha256(`${ip}:${ua}`); +} + +/** Issues a long-lived HTTP-only, SameSite=Strict secure session cookie on first visit, bound to client IP/UA, and rotates periodically. */ +export const sessionMiddleware: RequestHandler = (req, res, next) => { + const currentSid = req.cookies?.[SESSION_COOKIE]; + const clientHash = getClientFingerprint(req); + const now = Date.now(); + + if (!currentSid) { + const sid = randomBytes(32).toString('hex'); + res.cookie(SESSION_COOKIE, sid, { + httpOnly: true, + sameSite: 'strict', + secure: env.nodeEnv === 'production', + maxAge: ONE_YEAR_MS, + path: '/', + }); + req.cookies[SESSION_COOKIE] = sid; + sessionCache.set(sid, { clientHash, expiresAt: now + ROTATION_INTERVAL_MS }); + } else { + if (!sessionCache.has(currentSid)) { + sessionCache.set(currentSid, { clientHash, expiresAt: now + ROTATION_INTERVAL_MS }); + } + const cached = sessionCache.get(currentSid)!; + // Bind session to the client's fingerprint to prevent cookie hijacking + if (cached.clientHash !== clientHash) { + // Fingerprint mismatch — clear cookie, force new session + res.clearCookie(SESSION_COOKIE); + const sid = randomBytes(32).toString('hex'); + res.cookie(SESSION_COOKIE, sid, { + httpOnly: true, + sameSite: 'strict', + secure: env.nodeEnv === 'production', + maxAge: ONE_YEAR_MS, + path: '/', + }); + req.cookies[SESSION_COOKIE] = sid; + sessionCache.set(sid, { clientHash, expiresAt: now + ROTATION_INTERVAL_MS }); + } else if (now > cached.expiresAt) { + // Periodic rotation logic: rotate the session cookie value to limit lifetime of any stolen session id + sessionCache.delete(currentSid); + const newSid = randomBytes(32).toString('hex'); + res.cookie(SESSION_COOKIE, newSid, { + httpOnly: true, + sameSite: 'strict', + secure: env.nodeEnv === 'production', + maxAge: ONE_YEAR_MS, + path: '/', + }); + req.cookies[SESSION_COOKIE] = newSid; + sessionCache.set(newSid, { clientHash, expiresAt: now + ROTATION_INTERVAL_MS }); + + // Migrate file ownership to the new session ID so uploads survive rotation. + store.update((db: DatabaseShape) => { + for (const file of db.files) { + if (file.sessionId === currentSid) { + file.sessionId = newSid; + } + } + }).catch(() => {}); + } + } + next(); +}; + +export function getSessionId(req: Request): string { + return req.cookies?.[SESSION_COOKIE] ?? ''; +}