From 14d079bb359602502e475fe2157d6d4c9f79fca2 Mon Sep 17 00:00:00 2001 From: isnowglobal-admin Date: Tue, 30 Jun 2026 11:40:20 -0400 Subject: [PATCH] Redis-backed session store + session secret hardened + cookie sameSite=lax + 7d maxAge --- package-lock.json | 112 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 + server/routes.ts | 37 ++++++++++----- 3 files changed, 140 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 34d1425..c2d2e5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "connect-redis": "^9.0.0", "cors": "^2.8.6", "date-fns": "^3.6.0", "dotenv": "^16.4.7", @@ -71,6 +72,7 @@ "react-icons": "^5.4.0", "react-resizable-panels": "^2.1.7", "recharts": "^2.15.2", + "redis": "^6.0.1", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "tw-animate-css": "^1.2.5", @@ -3134,6 +3136,78 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@redis/bloom": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-6.0.1.tgz", + "integrity": "sha512-6ys5hhea+47n7o97ZFI4GvdzTQk/arIsXZgH159l6IVtJ4rZaB+KVdAfwvIxlmGA7z+NNlO8UxjTeQrenqjZcQ==", + "license": "MIT", + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@redis/client": "^6.0.1" + } + }, + "node_modules/@redis/client": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-6.0.1.tgz", + "integrity": "sha512-SwYl64hKHE/NeO2VSSG1y/4zIm0cNepyOZtQrOpLiNRHmH2FdWBOecNzsLiXCQdFCF9MCyoPXwAbaG2iMO0A7Q==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@node-rs/xxhash": "^1.1.0", + "@opentelemetry/api": ">=1 <2" + }, + "peerDependenciesMeta": { + "@node-rs/xxhash": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@redis/json": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-6.0.1.tgz", + "integrity": "sha512-KD1OztCYh7O9TkKMU9qZcFIKoudIGqmgXsOhQVq5A3REGrnl+wg0kporQFQCO+fcxe/nhvDgmBtXrm3diPGczA==", + "license": "MIT", + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@redis/client": "^6.0.1" + } + }, + "node_modules/@redis/search": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-6.0.1.tgz", + "integrity": "sha512-G09OujS3eOtQnP7kZC5eZTiazwgeimlo6Pf3vHnE1jO7rfqrtmMI0R1/ZXfzoW8p9vB4QiH538aEsWaHKd8l5w==", + "license": "MIT", + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@redis/client": "^6.0.1" + } + }, + "node_modules/@redis/time-series": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-6.0.1.tgz", + "integrity": "sha512-8aLGSDtCpnPTLD7lEiHHmuDCFppctLdT8geFIDf/7LWV9y8Vre6RB+aBZrgkeo3X1oPmTt1IbVAQVxsuJvkODw==", + "license": "MIT", + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@redis/client": "^6.0.1" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -4661,6 +4735,15 @@ "node": ">=6" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/cmdk": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", @@ -4686,6 +4769,19 @@ "node": ">= 6" } }, + "node_modules/connect-redis": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-9.0.0.tgz", + "integrity": "sha512-QwzyvUePTMvEzG1hy45gZYw3X3YHrjmEdSkayURlcZft7hqadQ3X39wYkmCqblK2rGlw+XItELYt6GnyG6DEIQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "express-session": ">=1", + "redis": ">=5" + } + }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -7520,6 +7616,22 @@ "decimal.js-light": "^2.4.1" } }, + "node_modules/redis": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/redis/-/redis-6.0.1.tgz", + "integrity": "sha512-54FoTBdFw10Y602pShvk8CGJlSH55nY+CNAZaVk8YdxY3rENihdYm2lXrujrtupYTHyrVSZUxOdeStNQbNvnQg==", + "license": "MIT", + "dependencies": { + "@redis/bloom": "6.0.1", + "@redis/client": "6.0.1", + "@redis/json": "6.0.1", + "@redis/search": "6.0.1", + "@redis/time-series": "6.0.1" + }, + "engines": { + "node": ">= 20.0.0" + } + }, "node_modules/regexparam": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-3.0.0.tgz", diff --git a/package.json b/package.json index 54ddd70..eebfcc1 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "connect-redis": "^9.0.0", "cors": "^2.8.6", "date-fns": "^3.6.0", "dotenv": "^16.4.7", @@ -73,6 +74,7 @@ "react-icons": "^5.4.0", "react-resizable-panels": "^2.1.7", "recharts": "^2.15.2", + "redis": "^6.0.1", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "tw-animate-css": "^1.2.5", diff --git a/server/routes.ts b/server/routes.ts index 6436959..4b71242 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -1,11 +1,9 @@ import type { Express, Request, Response } from "express"; import { createServer } from "node:http"; import type { Server } from "node:http"; -// eslint-disable-next-line @typescript-eslint/no-var-requires import * as session from "express-session"; -// eslint-disable-next-line @typescript-eslint/no-var-requires -import MemoryStoreModule from "memorystore"; -const MemoryStore = MemoryStoreModule(session.default || session); +import { RedisStore } from "connect-redis"; +import { createClient } from "redis"; import { SYMBOLS, computeExpirations as mockExpirations, @@ -19,8 +17,6 @@ import { registerAuthRoutes, requireAuth } from "./authRoutes"; import { initDb, getAllCustomSymbols, createCustomSymbol, deleteCustomSymbol } from "./db"; import { rateLimit, securityHeaders } from "./middleware"; -const SessionMemoryStore = new MemoryStore({ checkPeriod: 86400000 }); - // Built-in tickers from marketData const BUILTIN_TICKERS = new Set(SYMBOLS.map((s) => s.ticker)); @@ -83,18 +79,37 @@ export async function registerRoutes(httpServer: Server, app: Express): Promise< // Auth-specific rate limit: 50 requests per 15 minutes (relaxed for dev) app.use("/api/auth/", rateLimit({ windowMs: 15 * 60 * 1000, max: 50 })); - // ── Session Configuration ───────────────────────────────────────── + // ── Session Configuration (Redis-backed for persistence) ────────── + const redisClient = createClient({ + url: process.env.REDIS_URL || "redis://127.0.0.1:6379", + }); + redisClient.connect().catch((err) => { + console.error("[session] Redis connection failed, sessions will be in-memory only:", err); + }); + const redisSessionStore = new RedisStore({ client: redisClient }); + app.use( - (session.default || session)({ - store: new MemoryStore({ checkPeriod: 86400000 }), + session.default?.({ + store: redisSessionStore, secret: process.env.SESSION_SECRET || "gammadesk-dev-secret-change-me", resave: false, saveUninitialized: false, cookie: { secure: process.env.NODE_ENV === "production", httpOnly: true, // Prevent XSS from accessing session cookie - sameSite: "strict", // Prevent CSRF - maxAge: 24 * 60 * 60 * 1000, // 24 hours + sameSite: "lax", // Allow same-site + top-level navigation + maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days + }, + }) || session({ + store: redisSessionStore, + secret: process.env.SESSION_SECRET || "gammadesk-dev-secret-change-me", + resave: false, + saveUninitialized: false, + cookie: { + secure: process.env.NODE_ENV === "production", + httpOnly: true, + sameSite: "lax", + maxAge: 7 * 24 * 60 * 60 * 1000, }, }), );