fix(media): block symlink traversal
parent
b94b220156
commit
2cf134668c
|
|
@ -6,6 +6,7 @@
|
||||||
- Hardened the relay IPC socket: now lives under `~/.warelay/ipc`, enforces 0700 dir / 0600 socket perms, rejects symlink or foreign-owned paths, and includes unit tests to lock in the behavior.
|
- Hardened the relay IPC socket: now lives under `~/.warelay/ipc`, enforces 0700 dir / 0600 socket perms, rejects symlink or foreign-owned paths, and includes unit tests to lock in the behavior.
|
||||||
- `warelay logout` now also prunes the shared session store (`~/.warelay/sessions.json`) alongside WhatsApp Web credentials, reducing leftover state after unlinking.
|
- `warelay logout` now also prunes the shared session store (`~/.warelay/sessions.json`) alongside WhatsApp Web credentials, reducing leftover state after unlinking.
|
||||||
- Logging now rolls daily to `/tmp/warelay/warelay-YYYY-MM-DD.log` (or custom dir) and prunes files older than 24h to reduce data retention.
|
- Logging now rolls daily to `/tmp/warelay/warelay-YYYY-MM-DD.log` (or custom dir) and prunes files older than 24h to reduce data retention.
|
||||||
|
- Media server now rejects symlinked files and ensures resolved paths stay inside the media directory, closing traversal via symlinks; added regression test.
|
||||||
|
|
||||||
## 1.3.0 — 2025-12-02
|
## 1.3.0 — 2025-12-02
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,4 +59,17 @@ describe("media server", () => {
|
||||||
expect(await res.text()).toBe("invalid path");
|
expect(await res.text()).toBe("invalid path");
|
||||||
await new Promise((r) => server.close(r));
|
await new Promise((r) => server.close(r));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("blocks symlink escaping outside media dir", async () => {
|
||||||
|
const target = path.join(process.cwd(), "package.json"); // outside MEDIA_DIR
|
||||||
|
const link = path.join(MEDIA_DIR, "link-out");
|
||||||
|
await fs.symlink(target, link);
|
||||||
|
|
||||||
|
const server = await startMediaServer(0, 5_000);
|
||||||
|
const port = (server.address() as AddressInfo).port;
|
||||||
|
const res = await fetch(`http://localhost:${port}/media/link-out`);
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
expect(await res.text()).toBe("invalid path");
|
||||||
|
await new Promise((r) => server.close(r));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,24 +17,31 @@ export function attachMediaRoutes(
|
||||||
|
|
||||||
app.get("/media/:id", async (req, res) => {
|
app.get("/media/:id", async (req, res) => {
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
const file = path.resolve(mediaDir, id);
|
const mediaRoot = (await fs.realpath(mediaDir)) + path.sep;
|
||||||
const mediaRoot = path.resolve(mediaDir) + path.sep;
|
const file = path.resolve(mediaRoot, id);
|
||||||
if (!file.startsWith(mediaRoot)) {
|
|
||||||
res.status(400).send("invalid path");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const stat = await fs.stat(file);
|
const lstat = await fs.lstat(file);
|
||||||
|
if (lstat.isSymbolicLink()) {
|
||||||
|
res.status(400).send("invalid path");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const realPath = await fs.realpath(file);
|
||||||
|
if (!realPath.startsWith(mediaRoot)) {
|
||||||
|
res.status(400).send("invalid path");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const stat = await fs.stat(realPath);
|
||||||
if (Date.now() - stat.mtimeMs > ttlMs) {
|
if (Date.now() - stat.mtimeMs > ttlMs) {
|
||||||
await fs.rm(file).catch(() => {});
|
await fs.rm(realPath).catch(() => {});
|
||||||
res.status(410).send("expired");
|
res.status(410).send("expired");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.sendFile(file);
|
res.sendFile(realPath);
|
||||||
// best-effort single-use cleanup after response ends
|
// best-effort single-use cleanup after response ends
|
||||||
res.on("finish", () => {
|
res.on("finish", () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
fs.rm(file).catch(() => {});
|
fs.rm(realPath).catch(() => {});
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue