feat(assets): verify ready exports and fix sandbox bridge
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user