diff --git a/apps/ignis-server/server/demo/demo-provision.js b/apps/ignis-server/server/demo/demo-provision.js index a4840d8..a361288 100644 --- a/apps/ignis-server/server/demo/demo-provision.js +++ b/apps/ignis-server/server/demo/demo-provision.js @@ -80,6 +80,14 @@ async function provisionVault(sessionId, userVaultName) { const storageName = makeStorageName(sessionId, userVaultName); const vaultPath = path.join(config.vaultRoot, storageName); + // keep the resolved path inside the vault root. + const root = path.resolve(config.vaultRoot); + const resolved = path.resolve(vaultPath); + + if (resolved !== root && !resolved.startsWith(root + path.sep)) { + return { error: "invalid-vault-name" }; + } + await fsp.mkdir(config.vaultRoot, { recursive: true }); try { diff --git a/apps/ignis-server/server/demo/demo-sessions.js b/apps/ignis-server/server/demo/demo-sessions.js index b39c421..3b35690 100644 --- a/apps/ignis-server/server/demo/demo-sessions.js +++ b/apps/ignis-server/server/demo/demo-sessions.js @@ -16,6 +16,13 @@ function newSessionId() { return crypto.randomBytes(12).toString("hex"); } +// accept only the format we issue. +const SESSION_ID_RE = /^[a-f0-9]{24}$/; + +function isValidSessionId(id) { + return typeof id === "string" && SESSION_ID_RE.test(id); +} + function prefixFor(sessionId) { return "demo-" + sessionId + PREFIX_SEPARATOR; } @@ -61,20 +68,25 @@ function setSessionCookie(res, sessionId) { res.setHeader( "Set-Cookie", - `${COOKIE_NAME}=${sessionId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAgeSeconds}`, + `${COOKIE_NAME}=${sessionId}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAgeSeconds}`, ); } // Resolve the session for a request. If none exists, create one (unless options.peek is true). function getOrCreateSession(req, res, options = {}) { const cookies = parseCookies(req); - const existing = cookies[COOKIE_NAME]; + const raw = cookies[COOKIE_NAME]; + const existing = isValidSessionId(raw) ? raw : null; if (existing && sessions.has(existing)) { return existing; } if (existing && !sessions.has(existing)) { + if (sessions.size >= config.demoMaxSessions) { + return null; + } + // Cookie outlived in-memory session. reuse the id to keep the prefix. sessions.set(existing, { lastActivity: Date.now(), diff --git a/apps/ignis-server/server/demo/demo-ws.js b/apps/ignis-server/server/demo/demo-ws.js index 6054464..e3eae83 100644 --- a/apps/ignis-server/server/demo/demo-ws.js +++ b/apps/ignis-server/server/demo/demo-ws.js @@ -10,6 +10,7 @@ const { sessions, parseCookies, makeStorageName, + tryParseUserVaultName, touchSession, } = require("./demo-sessions"); @@ -28,6 +29,20 @@ function wireWebSocket(server) { if (userVault && !userVault.startsWith("demo-")) { u.searchParams.set("vault", makeStorageName(sessionId, userVault)); req.url = u.pathname + u.search; + } else if ( + userVault && + userVault.startsWith("demo-") && + tryParseUserVaultName(sessionId, userVault) === null + ) { + // An already-prefixed vault that isn't this session's: refuse the upgrade. + const socket = rest[0]; + + if (socket && socket.writable) { + socket.write("HTTP/1.1 403 Forbidden\r\n\r\n"); + socket.destroy(); + } + + return; } touchSession(sessionId); diff --git a/apps/ignis-server/server/demo/index.js b/apps/ignis-server/server/demo/index.js index 11f1b3a..54bd592 100644 --- a/apps/ignis-server/server/demo/index.js +++ b/apps/ignis-server/server/demo/index.js @@ -70,6 +70,11 @@ function setupDemo(app) { // Hide server-side plugins (headless-sync) from the demo UI app.use("/api/plugins", pluginsBlocker); + // Plugin routes are not exposed in demo mode. + app.use("/api/ext", (req, res) => { + res.status(403).json({ error: "Plugin routes are disabled in demo mode" }); + }); + // Server settings are-fixed in demo mode. app.use("/api/settings", (req, res) => { res.status(403).json({ error: "Settings are disabled in demo mode" });