Import source: backend/src/middleware/session
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -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<string, { clientHash: string, expiresAt: number }>();
|
||||
|
||||
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] ?? '';
|
||||
}
|
||||
Reference in New Issue
Block a user