@@ -59,12 +59,48 @@ export async function runWorkspaceScript(
5959 ) ;
6060 }
6161
62- const scriptPath = getScriptPath ( workspacePath , scriptName ) ;
62+ // Resolve real paths to handle symlinks and prevent escape const scriptPath = getScriptPath(workspacePath, scriptName);
6363 const scriptsDir = getScriptsDir ( workspacePath ) ;
6464
65- // Use runtime-aware normalization to avoid OS-specific path mangling (e.g. SSH on Windows)
66- const normalizedScriptPath = runtime . normalizePath ( scriptPath , workspacePath ) ;
67- const normalizedScriptsDir = runtime . normalizePath ( scriptsDir , workspacePath ) ;
65+ let resolvedScriptPath : string ;
66+ let resolvedScriptsDir : string ;
67+
68+ try {
69+ // Use runtime.resolvePath (which should behave like realpath) if available,
70+ // otherwise rely on the runtime-specific normalization.
71+ // Ideally, we want `realpath` behavior here.
72+ // Since the Runtime interface doesn't strictly expose `realpath`, we'll rely on
73+ // the filesystem (via runtime.exec or similar) or assume normalizePath+standard checks are mostly sufficient.
74+ // HOWEVER, for local runtime we can use fs.realpath. For SSH, we might need a command.
75+ // To keep it simple and robust within the existing abstractions:
76+ // We will use the runtime to resolve the path if possible, but `runtime.resolvePath`
77+ // is documented to expand tildes, not necessarily resolve symlinks (though it often does).
78+
79+ // BUT, to address the specific review concern about symlinks:
80+ // We should try to get the canonical path.
81+ // Note: checking containment purely by string path on un-resolved paths is weak against symlinks.
82+
83+ // Strategy:
84+ // 1. Get the script path (constructed from workspace + script name).
85+ // 2. Get the scripts dir.
86+ // 3. Ask runtime to resolve them to absolute, canonical paths (resolving symlinks).
87+ // (If runtime doesn't support explicit symlink resolution in its API, we might be limited).
88+ // The review implies we *should* do this.
89+ // Let's add a helper or use `runtime.resolvePath` which claims to resolve to "absolute, canonical form".
90+
91+ resolvedScriptPath = await runtime . resolvePath ( scriptPath ) ;
92+ resolvedScriptsDir = await runtime . resolvePath ( scriptsDir ) ;
93+ } catch {
94+ // If we can't resolve paths (e.g. file doesn't exist), we can't verify containment securely.
95+ // But we already established the script *must* exist in step 2 (which we moved up or will do).
96+ // Actually step 2 is below. Let's do existence check + resolution together or accept that
97+ // resolution failure implies non-existence.
98+ return Err ( `Script not found or inaccessible: ${ scriptName } ` ) ;
99+ }
100+
101+ // Use runtime-aware normalization on the RESOLVED paths
102+ const normalizedScriptPath = runtime . normalizePath ( resolvedScriptPath , workspacePath ) ;
103+ const normalizedScriptsDir = runtime . normalizePath ( resolvedScriptsDir , workspacePath ) ;
68104
69105 // Determine separator from the normalized path itself
70106 const separator = normalizedScriptsDir . includes ( "\\" ) ? "\\" : "/" ;
@@ -74,9 +110,9 @@ export async function runWorkspaceScript(
74110 return Err ( `Invalid script name: ${ scriptName } . Script path escapes scripts directory.` ) ;
75111 }
76112
77- // 2. Check existence
113+ // 2. Check existence (redundant if resolvePath succeeded, but good for specific error msg if it was a file/dir mismatch)
78114 try {
79- const stat = await runtime . stat ( scriptPath ) ;
115+ const stat = await runtime . stat ( resolvedScriptPath ) ;
80116 if ( stat . isDirectory ) {
81117 return Err ( `Script not found: .cmux/scripts/${ scriptName } ` ) ;
82118 }
@@ -130,7 +166,8 @@ export async function runWorkspaceScript(
130166
131167 // We use the scriptPath directly, but escape it safely using single quotes
132168 // to prevent shell injection (e.g. if script name contains quotes or backticks)
133- const safeScriptPath = scriptPath . replace ( / ' / g, "'\\''" ) ;
169+ // NOTE: We use the resolved path to ensure we run exactly what we validated
170+ const safeScriptPath = resolvedScriptPath . replace ( / ' / g, "'\\''" ) ;
134171 const command = `'${ safeScriptPath } '${ escapedArgs ? ` ${ escapedArgs } ` : "" } ` ;
135172
136173 // 5. Execute using createBashTool
0 commit comments