Files
recap-relay/server/test/meeting-path.test.js
T
Keysat cbd9748a79 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.
2026-06-13 18:22:00 -05:00

35 lines
1.3 KiB
JavaScript

// Path-traversal guard for meeting record ids (internal-meetings.js
// meetingPath). A caller-supplied :id must never escape the
// internal-meetings/ directory.
import { test, describe } from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
import { meetingPath } from "../routes/internal-meetings.js";
const DATA = "/data";
const DIR = path.join(DATA, "internal-meetings");
describe("meetingPath", () => {
test("a normal UUID id maps into internal-meetings/", () => {
const id = "2f1c9b3a-0e4d-4a77-9d2a-abc123def456";
assert.equal(meetingPath(DATA, id), path.join(DIR, `${id}.json`));
});
test("traversal-shaped ids are sanitized and stay inside the dir", () => {
for (const id of ["../../etc/passwd", "../../../root/.ssh/id", "..%2f..%2fx", "a/b/c", "....//x"]) {
const p = meetingPath(DATA, id);
const rel = path.relative(DIR, p);
assert.ok(!rel.startsWith(".."), `${id} escaped to ${p}`);
assert.ok(!p.includes(".."), `${id} left ".." in ${p}`);
assert.ok(p.endsWith(".json"));
}
});
test("an id that sanitizes to empty throws (load/delete catch → 404 / no-op)", () => {
for (const id of ["", null, undefined, "/", "../", "...", "!!!"]) {
assert.throws(() => meetingPath(DATA, id), /invalid meeting id/);
}
});
});