Guard meeting :id against path traversal

saveMeeting/loadMeeting/deleteMeeting built path.join(meetingsDir, id +
'.json') straight from req.params.id, so an admin-authed :id like
'../../etc/passwd' could read/write/delete outside internal-meetings/.
Centralize a meetingPath() helper that strips anything outside
[A-Za-z0-9_-] (mirrors output-store.js) and throws on an empty result;
load/delete catch it as 404/no-op. Add a regression test.
This commit is contained in:
Keysat
2026-06-13 18:22:00 -05:00
parent 54ddaffced
commit cbd9748a79
2 changed files with 49 additions and 3 deletions
+15 -3
View File
@@ -77,19 +77,31 @@ async function ensureMeetingsDir(dataDir) {
await fs.mkdir(meetingsDir(dataDir), { recursive: true }).catch(() => {});
}
// Build the on-disk path for a meeting record, sanitizing the id so a
// caller-supplied :id can't traverse out of internal-meetings/. Real
// ids are UUIDs; anything outside [A-Za-z0-9_-] is stripped (mirrors
// output-store.js's pathFor). Throws when the id sanitizes to empty —
// load/delete catch it (→ 404 / no-op); save only ever gets a freshly
// minted id.
export function meetingPath(dataDir, id) {
const safe = String(id || "").replace(/[^A-Za-z0-9_-]/g, "");
if (!safe) throw new Error("invalid meeting id");
return path.join(meetingsDir(dataDir), `${safe}.json`);
}
// ─── Storage layer ──────────────────────────────────────────────────
async function saveMeeting(dataDir, id, record) {
await ensureMeetingsDir(dataDir);
const filePath = path.join(meetingsDir(dataDir), `${id}.json`);
const filePath = meetingPath(dataDir, id);
await fs.writeFile(filePath, JSON.stringify(record, null, 2), {
mode: 0o600,
});
}
async function loadMeeting(dataDir, id) {
const filePath = path.join(meetingsDir(dataDir), `${id}.json`);
try {
const filePath = meetingPath(dataDir, id);
const raw = await fs.readFile(filePath, "utf8");
const rec = JSON.parse(raw);
// Retroactive chunk-contiguity backfill must run BEFORE the
@@ -239,8 +251,8 @@ async function listMeetings(dataDir) {
}
async function deleteMeeting(dataDir, id) {
const filePath = path.join(meetingsDir(dataDir), `${id}.json`);
try {
const filePath = meetingPath(dataDir, id);
await fs.unlink(filePath);
return true;
} catch {