harden demo sessions

This commit is contained in:
Nystik
2026-06-08 15:43:15 +02:00
parent 9d01ce71bc
commit 62d87af7dd
4 changed files with 42 additions and 2 deletions

View File

@@ -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 {

View File

@@ -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(),

View File

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

View File

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