mirror of
https://github.com/Nystik-gh/ignis.git
synced 2026-06-17 04:35:53 +00:00
harden shim origin check and fs/vault endpoints
This commit is contained in:
@@ -98,7 +98,7 @@ router.get("/stat", async (req, res) => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
res
|
res
|
||||||
.status(e.code === "ENOENT" ? 404 : 500)
|
.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) {
|
} catch (e) {
|
||||||
res
|
res
|
||||||
.status(e.code === "ENOENT" ? 404 : 500)
|
.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) {
|
} catch (e) {
|
||||||
res
|
res
|
||||||
.status(e.code === "ENOENT" ? 404 : 500)
|
.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);
|
invalidateBootstrap(req);
|
||||||
res.json({ ok: true, mtime: result.mtime, size: result.size });
|
res.json({ ok: true, mtime: result.mtime, size: result.size });
|
||||||
} catch (e) {
|
} 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);
|
invalidateBootstrap(req);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} 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);
|
invalidateBootstrap(req);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} 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);
|
invalidateBootstrap(req);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} 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);
|
invalidateBootstrap(req);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} 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
|
// File already gone - desired outcome achieved
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} else {
|
} 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);
|
invalidateBootstrap(req);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} 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);
|
invalidateBootstrap(req);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} 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) {
|
} catch (e) {
|
||||||
res
|
res
|
||||||
.status(e.code === "ENOENT" ? 404 : 500)
|
.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) });
|
res.json({ path: path.relative(req._vaultRoot, real) });
|
||||||
} catch (e) {
|
} 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);
|
invalidateBootstrap(req);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} 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 : [];
|
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) {
|
if (paths.length === 0) {
|
||||||
return res.json({ files: {} });
|
return res.json({ files: {} });
|
||||||
}
|
}
|
||||||
@@ -531,7 +536,7 @@ router.get("/tree", async (req, res) => {
|
|||||||
|
|
||||||
res.json(tree);
|
res.json(tree);
|
||||||
} catch (e) {
|
} 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) {
|
} catch (e) {
|
||||||
res
|
res
|
||||||
.status(e.code === "ENOENT" ? 404 : 500)
|
.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) {
|
} catch (e) {
|
||||||
res
|
res
|
||||||
.status(e.code === "ENOENT" ? 404 : 500)
|
.status(e.code === "ENOENT" ? 404 : 500)
|
||||||
.json({ error: e.message, code: e.code });
|
.json({ error: e.code || "internal", code: e.code });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,25 @@ const bootstrapRoutes = require("./bootstrap");
|
|||||||
|
|
||||||
const router = express.Router();
|
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)
|
// GET /api/vault/list - returns all discovered vaults (re-scans on each call)
|
||||||
router.get("/list", (req, res) => {
|
router.get("/list", (req, res) => {
|
||||||
config.refreshVaults();
|
config.refreshVaults();
|
||||||
@@ -41,7 +60,7 @@ router.get("/info", async (req, res) => {
|
|||||||
router.post("/create", async (req, res) => {
|
router.post("/create", async (req, res) => {
|
||||||
const name = req.body?.name;
|
const name = req.body?.name;
|
||||||
|
|
||||||
if (!name || /[\/\\:*?"<>|]/.test(name)) {
|
if (!isValidVaultName(name)) {
|
||||||
return res.status(400).json({ error: "Invalid vault 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" });
|
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 vaultId = req.body?.vault;
|
||||||
const newName = req.body?.name;
|
const newName = req.body?.name;
|
||||||
|
|
||||||
if (!newName || /[\/\\:*?"<>|]/.test(newName)) {
|
if (!isValidVaultName(newName)) {
|
||||||
return res.status(400).json({ error: "Invalid vault name" });
|
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" });
|
.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 });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message, code: e.code });
|
res.status(500).json({ error: e.code || "internal", code: e.code });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
// File descriptor shim - maps fake integer fds to in-memory file buffers.
|
// File descriptor shim. Maps fake integer fds to in-memory file buffers.
|
||||||
// Enables libraries like yauzl that use fs.open/fs.read/fs.close to seek
|
|
||||||
// around files without loading them via readFileSync upfront.
|
|
||||||
|
|
||||||
import { isInputCachePath, inputCacheGet } from "./input-cache.js";
|
import { isInputCachePath, inputCacheGet } from "./input-cache.js";
|
||||||
import { resolvePath } from "./transforms.js";
|
import { resolvePath } from "./transforms.js";
|
||||||
|
import { hasVirtualFile, getVirtualFile } from "./virtual-files.js";
|
||||||
|
|
||||||
let nextFd = 100;
|
let nextFd = 100;
|
||||||
const openFiles = new Map();
|
const openFiles = new Map();
|
||||||
@@ -24,6 +23,15 @@ export function createFdOps(metadataCache, contentCache, transport) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resolved = resolvePath(path);
|
const resolved = resolvePath(path);
|
||||||
|
|
||||||
|
if (hasVirtualFile(resolved)) {
|
||||||
|
const content = getVirtualFile(resolved);
|
||||||
|
|
||||||
|
return typeof content === "string"
|
||||||
|
? new TextEncoder().encode(content)
|
||||||
|
: content;
|
||||||
|
}
|
||||||
|
|
||||||
const cached = contentCache.get(resolved);
|
const cached = contentCache.get(resolved);
|
||||||
|
|
||||||
if (cached !== null) {
|
if (cached !== null) {
|
||||||
@@ -60,7 +68,11 @@ export function createFdOps(metadataCache, contentCache, transport) {
|
|||||||
const hasInCache = isInputCachePath(path) && inputCacheGet(path) !== null;
|
const hasInCache = isInputCachePath(path) && inputCacheGet(path) !== null;
|
||||||
const resolved = resolvePath(path);
|
const resolved = resolvePath(path);
|
||||||
|
|
||||||
if (!hasInCache && !metadataCache.has(resolved)) {
|
if (
|
||||||
|
!hasInCache &&
|
||||||
|
!hasVirtualFile(resolved) &&
|
||||||
|
!metadataCache.has(resolved)
|
||||||
|
) {
|
||||||
const err = new Error(
|
const err = new Error(
|
||||||
`ENOENT: no such file or directory, open '${path}'`,
|
`ENOENT: no such file or directory, open '${path}'`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
applyWriteTransform,
|
applyWriteTransform,
|
||||||
resolvePath,
|
resolvePath,
|
||||||
} from "./transforms.js";
|
} from "./transforms.js";
|
||||||
|
import { hasVirtualFile, getVirtualFile } from "./virtual-files.js";
|
||||||
|
|
||||||
export function createFsSync(metadataCache, contentCache, transport) {
|
export function createFsSync(metadataCache, contentCache, transport) {
|
||||||
return {
|
return {
|
||||||
@@ -70,6 +71,21 @@ export function createFsSync(metadataCache, contentCache, transport) {
|
|||||||
const wantText = encoding === "utf8" || encoding === "utf-8";
|
const wantText = encoding === "utf8" || encoding === "utf-8";
|
||||||
const resolved = resolvePath(path);
|
const resolved = resolvePath(path);
|
||||||
|
|
||||||
|
// Virtual plugin source overrides any cache or transport version.
|
||||||
|
if (hasVirtualFile(resolved)) {
|
||||||
|
const content = getVirtualFile(resolved);
|
||||||
|
|
||||||
|
if (wantText) {
|
||||||
|
return typeof content === "string"
|
||||||
|
? content
|
||||||
|
: new TextDecoder().decode(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof content === "string"
|
||||||
|
? new TextEncoder().encode(content)
|
||||||
|
: content;
|
||||||
|
}
|
||||||
|
|
||||||
const meta = metadataCache.get(resolved);
|
const meta = metadataCache.get(resolved);
|
||||||
if (meta && meta.type === "directory") {
|
if (meta && meta.type === "directory") {
|
||||||
const e = new Error("EISDIR: illegal operation on a directory, read");
|
const e = new Error("EISDIR: illegal operation on a directory, read");
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
function isSameOrigin(url) {
|
function isSameOrigin(url) {
|
||||||
if (
|
if (
|
||||||
!url ||
|
!url ||
|
||||||
url.startsWith("/") ||
|
(url.startsWith("/") && !url.startsWith("//")) ||
|
||||||
url.startsWith("./") ||
|
url.startsWith("./") ||
|
||||||
url.startsWith("../")
|
url.startsWith("../")
|
||||||
) {
|
) {
|
||||||
|
|||||||
25
packages/shim/src/util/url.test.js
Normal file
25
packages/shim/src/util/url.test.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import { isSameOrigin } from "./url.js";
|
||||||
|
|
||||||
|
describe("isSameOrigin", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
global.window = { location: { origin: "https://vault.example.com" } };
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete global.window;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats a root-relative path as same-origin", () => {
|
||||||
|
expect(isSameOrigin("/api/fs/readFile")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats a protocol-relative URL as cross-origin", () => {
|
||||||
|
expect(isSameOrigin("//evil.com/x")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches the page origin and rejects a different host", () => {
|
||||||
|
expect(isSameOrigin("https://vault.example.com/x")).toBe(true);
|
||||||
|
expect(isSameOrigin("https://evil.com/x")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user