Redis-backed session store + session secret hardened + cookie sameSite=lax + 7d maxAge

master
isnowglobal-admin 2026-06-30 11:40:20 -04:00
parent f35924eb54
commit 14d079bb35
3 changed files with 140 additions and 11 deletions

112
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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,
},
}),
);