feat(assets): verify ready exports and fix sandbox bridge

This commit is contained in:
2026-08-15 09:45:51 +02:00
parent 4ff160ee73
commit aa3a925e5a
6 changed files with 175 additions and 14 deletions
@@ -395,7 +395,7 @@ new file mode 100644
index 0000000..eda0255
--- /dev/null
+++ b/lib/projectcaptureSecurity.ts
@@ -0,0 +1,202 @@
@@ -0,0 +1,223 @@
+import path from "node:path";
+
+export const PROJECTCAPTURE_LOOPBACK_HOST = "127.0.0.1" as const;
@@ -411,15 +411,36 @@ index 0000000..eda0255
+
+export interface AssetSandboxFs {
+ existsSync(path: string): boolean;
+ lstatSync(path: string): {
+ lstatSync?(path: string): {
+ isDirectory(): boolean;
+ isFile(): boolean;
+ isSymbolicLink(): boolean;
+ };
+ statSync?(path: string): {
+ isDirectory(): boolean;
+ isFile(): boolean;
+ };
+ realpathSync(path: string): string;
+ mkdirSync(path: string, options?: { mode?: number }): unknown;
+}
+
+type AssetStat = {
+ isDirectory(): boolean;
+ isFile(): boolean;
+ isSymbolicLink?(): boolean;
+};
+
+function inspectPath(pathname: string, fs: AssetSandboxFs): AssetStat {
+ if (typeof fs.lstatSync === "function") return fs.lstatSync(pathname);
+ if (typeof fs.statSync === "function") return fs.statSync(pathname);
+ throw new Error("Blockbench filesystem bridge does not expose a safe stat operation.");
+}
+
+function isSymbolicLink(pathname: string, stat: AssetStat, fs: AssetSandboxFs): boolean {
+ if (typeof stat.isSymbolicLink === "function") return stat.isSymbolicLink();
+ return fs.realpathSync(pathname) !== path.resolve(pathname);
+}
+
+function requireSecretToken(value: string | undefined): string {
+ const token = value?.trim();
+ if (!token || token.length < PROJECTCAPTURE_MIN_TOKEN_LENGTH) {
@@ -520,8 +541,8 @@ index 0000000..eda0255
+ if (!fs.existsSync(root)) {
+ throw new Error("Configured ProjectCapture asset root does not exist.");
+ }
+ const rootStat = fs.lstatSync(root);
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
+ const rootStat = inspectPath(root, fs);
+ if (isSymbolicLink(root, rootStat, fs) || !rootStat.isDirectory()) {
+ throw new Error("Configured ProjectCapture asset root must be a real directory, not a symlink.");
+ }
+ return fs.realpathSync(root);
@@ -541,8 +562,8 @@ index 0000000..eda0255
+ cursor = path.join(cursor, segment);
+ assertInsideRoot(root, cursor);
+ if (fs.existsSync(cursor)) {
+ const stat = fs.lstatSync(cursor);
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
+ const stat = inspectPath(cursor, fs);
+ if (isSymbolicLink(cursor, stat, fs) || !stat.isDirectory()) {
+ throw new Error("Asset path contains a symlink or non-directory parent.");
+ }
+ } else {
@@ -568,8 +589,8 @@ index 0000000..eda0255
+ const candidate = path.resolve(root, relativePath);
+ assertInsideRoot(root, candidate);
+ if (fs.existsSync(candidate)) {
+ const stat = fs.lstatSync(candidate);
+ if (stat.isSymbolicLink() || !stat.isFile()) {
+ const stat = inspectPath(candidate, fs);
+ if (isSymbolicLink(candidate, stat, fs) || !stat.isFile()) {
+ throw new Error("Asset target must be a regular file and must not be a symlink.");
+ }
+ }
@@ -590,8 +611,8 @@ index 0000000..eda0255
+ if (!fs.existsSync(lexicalCandidate)) {
+ throw new Error("Requested asset does not exist.");
+ }
+ const stat = fs.lstatSync(lexicalCandidate);
+ if (stat.isSymbolicLink() || !stat.isFile()) {
+ const stat = inspectPath(lexicalCandidate, fs);
+ if (isSymbolicLink(lexicalCandidate, stat, fs) || !stat.isFile()) {
+ throw new Error("Requested asset must be a regular file and must not be a symlink.");
+ }
+ const realCandidate = fs.realpathSync(lexicalCandidate);
+1 -1
View File
@@ -5,7 +5,7 @@ TOOL_ROOT="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
MCP_REPOSITORY="https://github.com/jasonjgardner/blockbench-mcp-plugin.git"
MCP_COMMIT="6b069e308fdfc9b0a1c15bc924ca78150815f143"
MCP_PATCH="${TOOL_ROOT}/patches/0001-projectcapture-hardening.patch"
MCP_PATCH_SHA256="9de0b3767e96db7a68ef4c7082d7c76bf140b4932fc9550ad6e99d9a24a048c3"
MCP_PATCH_SHA256="092d5446fcdd1f28d6abcc931be2895727fd9ab3dd56728874b6847a614ed8fa"
BUN_VERSION="1.3.14"
require_command() {