harden shim origin check and fs/vault endpoints

This commit is contained in:
Nystik
2026-06-08 18:46:32 +02:00
parent 62d87af7dd
commit 542360c681
6 changed files with 104 additions and 27 deletions

View File

@@ -98,7 +98,7 @@ router.get("/stat", async (req, res) => {
} catch (e) {
res
.status(e.code === "ENOENT" ? 404 : 500)
.json({ error: e.message, code: e.code });
.json({ error: e.code || "internal", code: e.code });
}
});
@@ -133,7 +133,7 @@ router.get("/readdir", async (req, res) => {
} catch (e) {
res
.status(e.code === "ENOENT" ? 404 : 500)
.json({ error: e.message, code: e.code });
.json({ error: e.code || "internal", code: e.code });
}
});
@@ -184,7 +184,7 @@ router.get("/readFile", async (req, res) => {
} catch (e) {
res
.status(e.code === "ENOENT" ? 404 : 500)
.json({ error: e.message, code: e.code });
.json({ error: e.code || "internal", code: e.code });
}
});
@@ -213,7 +213,7 @@ router.post("/writeFile", async (req, res) => {
invalidateBootstrap(req);
res.json({ ok: true, mtime: result.mtime, size: result.size });
} catch (e) {
res.status(500).json({ error: e.message, code: e.code });
res.status(500).json({ error: e.code || "internal", code: e.code });
}
});
@@ -231,7 +231,7 @@ router.post("/appendFile", async (req, res) => {
invalidateBootstrap(req);
res.json({ ok: true });
} catch (e) {
res.status(500).json({ error: e.message, code: e.code });
res.status(500).json({ error: e.code || "internal", code: e.code });
}
});
@@ -251,7 +251,7 @@ router.post("/mkdir", async (req, res) => {
invalidateBootstrap(req);
res.json({ ok: true });
} catch (e) {
res.status(500).json({ error: e.message, code: e.code });
res.status(500).json({ error: e.code || "internal", code: e.code });
}
});
@@ -280,7 +280,7 @@ router.post("/rename", async (req, res) => {
invalidateBootstrap(req);
res.json({ ok: true });
} catch (e) {
res.status(500).json({ error: e.message, code: e.code });
res.status(500).json({ error: e.code || "internal", code: e.code });
}
});
@@ -309,7 +309,7 @@ router.post("/copyFile", async (req, res) => {
invalidateBootstrap(req);
res.json({ ok: true });
} catch (e) {
res.status(500).json({ error: e.message, code: e.code });
res.status(500).json({ error: e.code || "internal", code: e.code });
}
});
@@ -331,7 +331,7 @@ router.delete("/unlink", async (req, res) => {
// File already gone - desired outcome achieved
res.json({ ok: true });
} else {
res.status(500).json({ error: e.message, code: e.code });
res.status(500).json({ error: e.code || "internal", code: e.code });
}
}
});
@@ -350,7 +350,7 @@ router.delete("/rmdir", async (req, res) => {
invalidateBootstrap(req);
res.json({ ok: true });
} catch (e) {
res.status(500).json({ error: e.message, code: e.code });
res.status(500).json({ error: e.code || "internal", code: e.code });
}
});
@@ -370,7 +370,7 @@ router.delete("/rm", async (req, res) => {
invalidateBootstrap(req);
res.json({ ok: true });
} catch (e) {
res.status(500).json({ error: e.message, code: e.code });
res.status(500).json({ error: e.code || "internal", code: e.code });
}
});
@@ -388,7 +388,7 @@ router.get("/access", async (req, res) => {
} catch (e) {
res
.status(e.code === "ENOENT" ? 404 : 500)
.json({ error: e.message, code: e.code });
.json({ error: e.code || "internal", code: e.code });
}
});
@@ -404,7 +404,7 @@ router.get("/realpath", async (req, res) => {
res.json({ path: path.relative(req._vaultRoot, real) });
} catch (e) {
res.status(500).json({ error: e.message, code: e.code });
res.status(500).json({ error: e.code || "internal", code: e.code });
}
});
@@ -426,7 +426,7 @@ router.post("/utimes", async (req, res) => {
invalidateBootstrap(req);
res.json({ ok: true });
} catch (e) {
res.status(500).json({ error: e.message, code: e.code });
res.status(500).json({ error: e.code || "internal", code: e.code });
}
});
@@ -441,6 +441,11 @@ router.post("/batch-read", async (req, res) => {
const paths = Array.isArray(req.body?.paths) ? req.body.paths : [];
// The indexer prefetcher (the only caller) batches at 50, so a much larger list is not legitimate.
if (paths.length > 1000) {
return res.status(400).json({ error: "too many paths in batch-read" });
}
if (paths.length === 0) {
return res.json({ files: {} });
}
@@ -531,7 +536,7 @@ router.get("/tree", async (req, res) => {
res.json(tree);
} catch (e) {
res.status(500).json({ error: e.message, code: e.code });
res.status(500).json({ error: e.code || "internal", code: e.code });
}
});
@@ -561,7 +566,7 @@ router.get("/download", async (req, res) => {
} catch (e) {
res
.status(e.code === "ENOENT" ? 404 : 500)
.json({ error: e.message, code: e.code });
.json({ error: e.code || "internal", code: e.code });
}
});
@@ -599,7 +604,7 @@ router.get("/download-zip", async (req, res) => {
} catch (e) {
res
.status(e.code === "ENOENT" ? 404 : 500)
.json({ error: e.message, code: e.code });
.json({ error: e.code || "internal", code: e.code });
}
});

View File

@@ -6,6 +6,25 @@ const bootstrapRoutes = require("./bootstrap");
const router = express.Router();
// Vault names become directories under VAULT_ROOT; reject traversal, hidden, and reserved-device names.
const WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
function isValidVaultName(name) {
if (typeof name !== "string" || name.length === 0 || name.length > 255) {
return false;
}
if (/[\/\\:*?"<>|]/.test(name)) {
return false;
}
if (name.startsWith(".")) {
return false;
}
return !WINDOWS_RESERVED.test(name);
}
// GET /api/vault/list - returns all discovered vaults (re-scans on each call)
router.get("/list", (req, res) => {
config.refreshVaults();
@@ -41,7 +60,7 @@ router.get("/info", async (req, res) => {
router.post("/create", async (req, res) => {
const name = req.body?.name;
if (!name || /[\/\\:*?"<>|]/.test(name)) {
if (!isValidVaultName(name)) {
return res.status(400).json({ error: "Invalid vault name" });
}
@@ -62,7 +81,7 @@ router.post("/create", async (req, res) => {
return res.status(409).json({ error: "Vault already exists" });
}
res.status(500).json({ error: e.message, code: e.code });
res.status(500).json({ error: e.code || "internal", code: e.code });
}
});
@@ -71,7 +90,7 @@ router.post("/rename", async (req, res) => {
const vaultId = req.body?.vault;
const newName = req.body?.name;
if (!newName || /[\/\\:*?"<>|]/.test(newName)) {
if (!isValidVaultName(newName)) {
return res.status(400).json({ error: "Invalid vault name" });
}
@@ -98,7 +117,7 @@ router.post("/rename", async (req, res) => {
.json({ error: "A vault with that name already exists" });
}
res.status(500).json({ error: e.message, code: e.code });
res.status(500).json({ error: e.code || "internal", code: e.code });
}
});
@@ -119,7 +138,7 @@ router.delete("/remove", async (req, res) => {
res.json({ ok: true });
} catch (e) {
res.status(500).json({ error: e.message, code: e.code });
res.status(500).json({ error: e.code || "internal", code: e.code });
}
});