Skip to content

Conversation

@Shellishack
Copy link
Collaborator

No description provided.

@changeset-bot
Copy link

changeset-bot bot commented Oct 29, 2025

⚠️ No Changeset found

Latest commit: 747110c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Oct 29, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
web-editor Ready Ready Preview Comment Oct 29, 2025 0:05am

// List all folders in a path
async function handleListProjects(uri: string) {
const rootPath = uri;
const files = await fs.promises.readdir(rootPath, { withFileTypes: true });

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
This path depends on a
user-provided value
.
.map((file) => file.name)
.map((projectName) => ({
name: projectName,
ctime: fs.statSync(path.join(rootPath, projectName)).ctime,

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 17 days ago

General fix approach:
Implement a safe root directory for all file-system operations. User-provided URIs/paths should be resolved relative to that root directory and then normalized to remove any traversal (..) components. After normalization, verify that the resolved file path is still within the intended root directory—otherwise, reject the operation.

Detailed fix for current code:

  • Define a constant PROJECTS_ROOT at the top of the file to specify a safe base directory (e.g., /pulse-editor/projects).
  • In the handleListProjects function, for any incoming uri, resolve it relative to PROJECTS_ROOT using path.resolve(PROJECTS_ROOT, uri).
  • After resolving, ensure the final path starts with PROJECTS_ROOT (using path.resolve for both).
  • If the validation fails, throw an error or return an error response.
  • All uses of rootPath in this function should refer to the sanitized path, not the raw user input.

Changes required:

  • Define and use a safe PROJECTS_ROOT directory.
  • Amend handleListProjects to securely resolve and validate the path.
  • Ensure all rootPath usages in this method are with the checked, normalized value.
  • Add required error handling (e.g., throwing an error or returning an appropriate message if the check fails).

Suggested changeset 1
remote-workspace/src/servers/api-server/platform-api/handler.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/remote-workspace/src/servers/api-server/platform-api/handler.ts b/remote-workspace/src/servers/api-server/platform-api/handler.ts
--- a/remote-workspace/src/servers/api-server/platform-api/handler.ts
+++ b/remote-workspace/src/servers/api-server/platform-api/handler.ts
@@ -4,6 +4,7 @@
 
 // Define a safe root directory for projects. Can be overridden by env or configured as needed.
 
+const PROJECTS_ROOT = "/pulse-editor/projects"; // <-- Set this directory as your allowed root
 const settingsPath = path.join("/pulse-editor", "settings.json");
 
 export async function handlePlatformAPIRequest(
@@ -113,14 +114,19 @@
 
 // List all folders in a path
 async function handleListProjects(uri: string) {
-  const rootPath = uri;
-  const files = await fs.promises.readdir(rootPath, { withFileTypes: true });
+  // Sanitize and secure the rootPath by resolving it under PROJECTS_ROOT
+  const requestedPath = path.resolve(PROJECTS_ROOT, "." + path.sep + uri); // enforce relative
+  // Check that the requested path is still within PROJECTS_ROOT
+  if (!requestedPath.startsWith(path.resolve(PROJECTS_ROOT))) {
+    throw new Error("Access to the requested path is not allowed.");
+  }
+  const files = await fs.promises.readdir(requestedPath, { withFileTypes: true });
   const folders = files
     .filter((file) => file.isDirectory())
     .map((file) => file.name)
     .map((projectName) => ({
       name: projectName,
-      ctime: fs.statSync(path.join(rootPath, projectName)).ctime,
+      ctime: fs.statSync(path.join(requestedPath, projectName)).ctime,
     }));
 
   return folders;
EOF
@@ -4,6 +4,7 @@

// Define a safe root directory for projects. Can be overridden by env or configured as needed.

const PROJECTS_ROOT = "/pulse-editor/projects"; // <-- Set this directory as your allowed root
const settingsPath = path.join("/pulse-editor", "settings.json");

export async function handlePlatformAPIRequest(
@@ -113,14 +114,19 @@

// List all folders in a path
async function handleListProjects(uri: string) {
const rootPath = uri;
const files = await fs.promises.readdir(rootPath, { withFileTypes: true });
// Sanitize and secure the rootPath by resolving it under PROJECTS_ROOT
const requestedPath = path.resolve(PROJECTS_ROOT, "." + path.sep + uri); // enforce relative
// Check that the requested path is still within PROJECTS_ROOT
if (!requestedPath.startsWith(path.resolve(PROJECTS_ROOT))) {
throw new Error("Access to the requested path is not allowed.");
}
const files = await fs.promises.readdir(requestedPath, { withFileTypes: true });
const folders = files
.filter((file) => file.isDirectory())
.map((file) => file.name)
.map((projectName) => ({
name: projectName,
ctime: fs.statSync(path.join(rootPath, projectName)).ctime,
ctime: fs.statSync(path.join(requestedPath, projectName)).ctime,
}));

return folders;
Copilot is powered by AI and may make mistakes. Always verify output.
baseUri: string | undefined = undefined,
) {
const rootPath = uri;
const files = await fs.promises.readdir(rootPath, { withFileTypes: true });

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
This path depends on a
user-provided value
.

Copilot Autofix

AI 17 days ago

The best way to fix the issue is to restrict all user-supplied paths involved in filesystem operations to a safe root directory (e.g., /pulse-editor). This involves:

  • Defining a constant, such as PROJECTS_ROOT, that points to the root folder under which all filesystem operations must take place.
  • Whenever a path (such as uri) is received from the user, normalize it by resolving it against PROJECTS_ROOT using path.resolve.
  • After resolving, use fs.realpathSync (or an async equivalent) to collapse symbolic links and further normalize the path.
  • After normalization, validate that the resolved path starts with the safe root directory (PROJECTS_ROOT) before proceeding. If not, reject the operation.
  • Apply these checks in every handler that receives and uses user path input: handleListProjects, listPathContent, handleListPathContent, and similar handlers.

This approach ensures that even if a user tries to supply "../../etc" or similar, the final path will never escape the designated project root.

Suggested changeset 1
remote-workspace/src/servers/api-server/platform-api/handler.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/remote-workspace/src/servers/api-server/platform-api/handler.ts b/remote-workspace/src/servers/api-server/platform-api/handler.ts
--- a/remote-workspace/src/servers/api-server/platform-api/handler.ts
+++ b/remote-workspace/src/servers/api-server/platform-api/handler.ts
@@ -4,8 +4,41 @@
 
 // Define a safe root directory for projects. Can be overridden by env or configured as needed.
 
-const settingsPath = path.join("/pulse-editor", "settings.json");
+// Safe root directory for all projects (can be adjusted as needed/environment/config)
+const PROJECTS_ROOT = "/pulse-editor";
 
+// Path sanitizer function: resolves and validates a user-supplied path under root
+function sanitizeProjectPath(userPath: string): string {
+  // Disallow undefined, null, or non-string
+  if (typeof userPath !== "string") {
+    throw new Error("Invalid path type");
+  }
+  // Remove null bytes
+  userPath = userPath.replace(/\0/g, "");
+  // Remove surrounding whitespace
+  userPath = userPath.trim();
+  // Do not allow absolute paths
+  if (path.isAbsolute(userPath)) {
+    // Instead, treat absolute paths as relative to PROJECTS_ROOT (drop leading "/")
+    userPath = path.relative("/", userPath);
+  }
+  // Resolve against PROJECTS_ROOT
+  const resolved = path.resolve(PROJECTS_ROOT, userPath);
+  // Normalize symlinks as well
+  let realResolved: string;
+  try {
+    realResolved = fs.realpathSync(resolved);
+  } catch (e) {
+    // Path doesn't exist yet (e.g., mkdir), use resolved
+    realResolved = resolved;
+  }
+  if (!realResolved.startsWith(PROJECTS_ROOT)) {
+    throw new Error("Attempted access outside project root");
+  }
+  return realResolved;
+}
+
+const settingsPath = path.join(PROJECTS_ROOT, "settings.json");
 export async function handlePlatformAPIRequest(
   data: {
     operation: string;
@@ -113,7 +145,12 @@
 
 // List all folders in a path
 async function handleListProjects(uri: string) {
-  const rootPath = uri;
+  let rootPath: string;
+  try {
+    rootPath = sanitizeProjectPath(uri);
+  } catch (e) {
+    throw new Error("Invalid path: " + (e.message ?? e));
+  }
   const files = await fs.promises.readdir(rootPath, { withFileTypes: true });
   const folders = files
     .filter((file) => file.isDirectory())
@@ -131,9 +168,13 @@
   options: any,
   baseUri: string | undefined = undefined,
 ) {
-  const rootPath = uri;
+  let rootPath: string;
+  try {
+    rootPath = sanitizeProjectPath(uri);
+  } catch (e) {
+    throw new Error("Invalid path: " + (e.message ?? e));
+  }
   const files = await fs.promises.readdir(rootPath, { withFileTypes: true });
-
   const promise: Promise<any>[] = files
     // Filter by file type
     .filter(
@@ -160,21 +200,27 @@
     .map(async (file) => {
       const name = file.name;
       const absoluteUri = path.join(rootPath, name);
+      let sanitizedAbsoluteUri: string;
+      try {
+        sanitizedAbsoluteUri = sanitizeProjectPath(path.relative(PROJECTS_ROOT, absoluteUri));
+      } catch (e) {
+        sanitizedAbsoluteUri = PROJECTS_ROOT; // fallback, shouldn't happen
+      }
       if (file.isDirectory()) {
         return {
           name: name,
           isFolder: true,
           subDirItems: options.isRecursive
-            ? await listPathContent(absoluteUri, options, baseUri ?? uri)
+            ? await listPathContent(sanitizedAbsoluteUri, options, baseUri ?? uri)
             : [],
-          uri: absoluteUri.replace(/\\/g, "/"),
+          uri: sanitizedAbsoluteUri.replace(/\\/g, "/"),
         };
       }
 
       return {
         name,
         isFolder: false,
-        uri: absoluteUri.replace(/\\/g, "/"),
+        uri: sanitizedAbsoluteUri.replace(/\\/g, "/"),
       };
     });
 
@@ -187,8 +221,14 @@
 }
 
 async function handleCreateProject(uri: string) {
-  // Create a folder at the validated path
-  await fs.promises.mkdir(uri);
+  // Validate and sanitize the path to create
+  let targetPath: string;
+  try {
+    targetPath = sanitizeProjectPath(uri);
+  } catch (e) {
+    throw new Error("Invalid path: " + (e.message ?? e));
+  }
+  await fs.promises.mkdir(targetPath);
 }
 
 async function handleDeleteProject(uri: string) {
EOF
Copilot is powered by AI and may make mistakes. Always verify output.

async function handleCreateProject(uri: string) {
// Create a folder at the validated path
await fs.promises.mkdir(uri);

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 17 days ago

The appropriate fix is to validate and restrict all user-supplied paths before using them in file operations. The best approach is:

  1. Define a safe root directory for all operations (for example, /pulse-editor/projects).
  2. For any API accepting a path from the user, first normalize (resolve) the path to get rid of things like ../, and then verify that the resulting absolute path is still under the intended root.
  3. If the resulting path lies outside the root, reject the operation.

This logic should be implemented once in a utility function (e.g., getSafePath(uri: string): string), used anywhere a user-supplied path is accessed. This function should:

  • Use path.resolve(ROOT_DIR, uri) to normalize against the project root.
  • Optionally canonicalize further via fs.realpathSync() if necessary.
  • Ensure the resulting path starts with the project root absolute path.

Apply this function to all uses of the user-supplied uri in file operations (handleCreateProject, handleDeleteProject, etc.).

You'll need:

  • A constant for the project root.
  • The helper function.
  • Modification of all functions to use the safe path before all filesystem access.

Suggested changeset 1
remote-workspace/src/servers/api-server/platform-api/handler.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/remote-workspace/src/servers/api-server/platform-api/handler.ts b/remote-workspace/src/servers/api-server/platform-api/handler.ts
--- a/remote-workspace/src/servers/api-server/platform-api/handler.ts
+++ b/remote-workspace/src/servers/api-server/platform-api/handler.ts
@@ -4,8 +4,22 @@
 
 // Define a safe root directory for projects. Can be overridden by env or configured as needed.
 
+// Change as necessary to match the actual safe storage location
+const PROJECTS_ROOT = "/pulse-editor/projects";
 const settingsPath = path.join("/pulse-editor", "settings.json");
 
+// Helper to ensure all user-supplied paths are safely resolved within the project root
+function getSafePath(userInputPath: string): string {
+  // Normalize/resolve the path against the project root
+  const resolvedPath = path.resolve(PROJECTS_ROOT, userInputPath);
+  // Optionally, handle "~" at the start of userInputPath
+  // (add your own logic here if necessary)
+  // Block paths outside the workspace
+  if (!resolvedPath.startsWith(PROJECTS_ROOT)) {
+    throw new Error("Path outside of allowed project root");
+  }
+  return resolvedPath;
+}
 export async function handlePlatformAPIRequest(
   data: {
     operation: string;
@@ -131,7 +143,7 @@
   options: any,
   baseUri: string | undefined = undefined,
 ) {
-  const rootPath = uri;
+  const rootPath = getSafePath(uri);
   const files = await fs.promises.readdir(rootPath, { withFileTypes: true });
 
   const promise: Promise<any>[] = files
@@ -188,12 +200,14 @@
 
 async function handleCreateProject(uri: string) {
   // Create a folder at the validated path
-  await fs.promises.mkdir(uri);
+  const safePath = getSafePath(uri);
+  await fs.promises.mkdir(safePath);
 }
 
 async function handleDeleteProject(uri: string) {
   // Delete the folder at the validated path
-  await fs.promises.rm(uri, { recursive: true, force: true });
+  const safePath = getSafePath(uri);
+  await fs.promises.rm(safePath, { recursive: true, force: true });
 }
 
 async function handleUpdateProject(
@@ -203,42 +213,50 @@
     ctime?: Date;
   },
 ) {
-  const newUri = path.join(path.dirname(uri), updatedInfo.name);
-  await fs.promises.rename(uri, newUri);
+  const safeOldPath = getSafePath(uri);
+  const safeNewPath = getSafePath(path.join(path.dirname(uri), updatedInfo.name));
+  await fs.promises.rename(safeOldPath, safeNewPath);
 }
 
 async function handleCreateFolder(uri: string) {
   // Create a folder at the validated path
-  await fs.promises.mkdir(uri);
+  const safePath = getSafePath(uri);
+  await fs.promises.mkdir(safePath);
 }
 
 async function handleCreateFile(uri: string) {
   // Create a file at the validated path
-  await fs.promises.writeFile(uri, "");
+  const safePath = getSafePath(uri);
+  await fs.promises.writeFile(safePath, "");
 }
 
 async function handleRename(oldUri: string, newUri: string) {
+  const safeOldPath = getSafePath(oldUri);
+  const safeNewPath = getSafePath(newUri);
   await fs.promises.rename(
-    oldUri,
-    newUri,
+    safeOldPath,
+    safeNewPath,
   );
 }
 
 async function handleDelete(uri: string) {
-  await fs.promises.rm(uri, {
+  const safePath = getSafePath(uri);
+  await fs.promises.rm(safePath, {
     recursive: true,
     force: true,
   });
 }
 
 async function handleHasPath(uri: string) {
-  return fs.existsSync(uri);
+  const safePath = getSafePath(uri);
+  return fs.existsSync(safePath);
 }
 
 async function handleReadFile(uri: string) {
   // Read the file at validated path
+  const safePath = getSafePath(uri);
   const data = await fs.promises.readFile(
-    uri,
+    safePath,
     "utf-8",
   );
 
@@ -247,7 +238,7 @@
 
 async function handleWriteFile(data: any, uri: string) {
   // Write the data at validated path
-  const safePath = uri;
+  const safePath = getSafePath(uri);
   // create parent directory if it doesn't exist
   const dir = path.dirname(safePath);
   if (!fs.existsSync(dir)) {
@@ -259,9 +250,11 @@
 
 async function handleCopyFiles(from: string, to: string) {
   // Copy the files from the validated from path to the validated to path
+  const safeFrom = getSafePath(from);
+  const safeTo = getSafePath(to);
   await fs.promises.cp(
-    from,
-    to,
+    safeFrom,
+    safeTo,
     {
       recursive: true,
     },
EOF
Copilot is powered by AI and may make mistakes. Always verify output.

async function handleDeleteProject(uri: string) {
// Delete the folder at the validated path
await fs.promises.rm(uri, { recursive: true, force: true });

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 17 days ago

The best way to fix the problem is to validate and restrict all user-supplied file/folder paths before performing any filesystem operations (create, delete, list, etc.). A robust approach is to define a safe root directory for all project operations (for example /pulse-editor), and ensure that all operations happen only within subdirectories of this root. To enforce this, before executing any operation like fs.promises.rm(uri, ...), normalize the requested uri (using path.resolve or similar), and confirm that it starts with the configured root path. If not, throw an error or deny the request.

The required changes are:

  • Define a root directory constant (reuse or clarify /pulse-editor as appropriate).
  • For each function that accepts a uri parameter and operates on the filesystem (handleDeleteProject, handleCreateProject, handleUpdateProject, etc.), add code to resolve and validate the path:
    • Use path.resolve(ROOT, uri) or similar.
    • (Optionally): Use fs.realpathSync for symlink safety.
    • Ensure the result begins with the root path.
    • If validation fails, throw an error or return a forbidden response.
  • Change all further references to the original uri to use the validated/normalized path.
  • The change should be done in remote-workspace/src/servers/api-server/platform-api/handler.ts, in each handler that uses user-supplied URIs.

No external libraries are needed beyond path and fs, which are already imported.

Suggested changeset 1
remote-workspace/src/servers/api-server/platform-api/handler.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/remote-workspace/src/servers/api-server/platform-api/handler.ts b/remote-workspace/src/servers/api-server/platform-api/handler.ts
--- a/remote-workspace/src/servers/api-server/platform-api/handler.ts
+++ b/remote-workspace/src/servers/api-server/platform-api/handler.ts
@@ -4,8 +4,24 @@
 
 // Define a safe root directory for projects. Can be overridden by env or configured as needed.
 
-const settingsPath = path.join("/pulse-editor", "settings.json");
+const ROOT = "/pulse-editor"; // Define safe project root
+const settingsPath = path.join(ROOT, "settings.json");
 
+// Validate path is under root directory
+function validatePathUnderRoot(uri: string): string {
+    const resolvedPath = path.resolve(ROOT, uri);
+    // Optionally use realpathSync for symlink safety. Try/catch, fallback to resolvedPath if it fails.
+    let realPath;
+    try {
+        realPath = fs.realpathSync(resolvedPath);
+    } catch {
+        realPath = resolvedPath;
+    }
+    if (!realPath.startsWith(ROOT)) {
+        throw new Error("Forbidden path detected");
+    }
+    return realPath;
+}
 export async function handlePlatformAPIRequest(
   data: {
     operation: string;
@@ -187,13 +202,15 @@
 }
 
 async function handleCreateProject(uri: string) {
-  // Create a folder at the validated path
-  await fs.promises.mkdir(uri);
+  // Validate and create folder
+  const safePath = validatePathUnderRoot(uri);
+  await fs.promises.mkdir(safePath);
 }
 
 async function handleDeleteProject(uri: string) {
-  // Delete the folder at the validated path
-  await fs.promises.rm(uri, { recursive: true, force: true });
+  // Validate and delete folder
+  const safePath = validatePathUnderRoot(uri);
+  await fs.promises.rm(safePath, { recursive: true, force: true });
 }
 
 async function handleUpdateProject(
@@ -203,13 +217,16 @@
     ctime?: Date;
   },
 ) {
-  const newUri = path.join(path.dirname(uri), updatedInfo.name);
-  await fs.promises.rename(uri, newUri);
+  const safeOldPath = validatePathUnderRoot(uri);
+  const newUri = path.join(path.dirname(safeOldPath), updatedInfo.name);
+  const safeNewPath = validatePathUnderRoot(path.relative(ROOT, newUri));
+  await fs.promises.rename(safeOldPath, safeNewPath);
 }
 
 async function handleCreateFolder(uri: string) {
-  // Create a folder at the validated path
-  await fs.promises.mkdir(uri);
+  // Validate and create folder
+  const safePath = validatePathUnderRoot(uri);
+  await fs.promises.mkdir(safePath);
 }
 
 async function handleCreateFile(uri: string) {
EOF
@@ -4,8 +4,24 @@

// Define a safe root directory for projects. Can be overridden by env or configured as needed.

const settingsPath = path.join("/pulse-editor", "settings.json");
const ROOT = "/pulse-editor"; // Define safe project root
const settingsPath = path.join(ROOT, "settings.json");

// Validate path is under root directory
function validatePathUnderRoot(uri: string): string {
const resolvedPath = path.resolve(ROOT, uri);
// Optionally use realpathSync for symlink safety. Try/catch, fallback to resolvedPath if it fails.
let realPath;
try {
realPath = fs.realpathSync(resolvedPath);
} catch {
realPath = resolvedPath;
}
if (!realPath.startsWith(ROOT)) {
throw new Error("Forbidden path detected");
}
return realPath;
}
export async function handlePlatformAPIRequest(
data: {
operation: string;
@@ -187,13 +202,15 @@
}

async function handleCreateProject(uri: string) {
// Create a folder at the validated path
await fs.promises.mkdir(uri);
// Validate and create folder
const safePath = validatePathUnderRoot(uri);
await fs.promises.mkdir(safePath);
}

async function handleDeleteProject(uri: string) {
// Delete the folder at the validated path
await fs.promises.rm(uri, { recursive: true, force: true });
// Validate and delete folder
const safePath = validatePathUnderRoot(uri);
await fs.promises.rm(safePath, { recursive: true, force: true });
}

async function handleUpdateProject(
@@ -203,13 +217,16 @@
ctime?: Date;
},
) {
const newUri = path.join(path.dirname(uri), updatedInfo.name);
await fs.promises.rename(uri, newUri);
const safeOldPath = validatePathUnderRoot(uri);
const newUri = path.join(path.dirname(safeOldPath), updatedInfo.name);
const safeNewPath = validatePathUnderRoot(path.relative(ROOT, newUri));
await fs.promises.rename(safeOldPath, safeNewPath);
}

async function handleCreateFolder(uri: string) {
// Create a folder at the validated path
await fs.promises.mkdir(uri);
// Validate and create folder
const safePath = validatePathUnderRoot(uri);
await fs.promises.mkdir(safePath);
}

async function handleCreateFile(uri: string) {
Copilot is powered by AI and may make mistakes. Always verify output.
const safePath = uri;
// create parent directory if it doesn't exist
const dir = path.dirname(safePath);
if (!fs.existsSync(dir)) {

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
// create parent directory if it doesn't exist
const dir = path.dirname(safePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 17 days ago

The best fix is to enforce that any filesystem path affected by user input cannot escape a designated safe root directory (such as /pulse-editor). This should be done by resolving the provided path relative to the root, normalizing and de-referencing it, and checking that the result begins with the root directory.

For every handler where a user-supplied path is being used as a filesystem path (e.g., the uri parameter in read/write/delete/has-path/copy-files etc.), wrap the path resolution as follows:

  • Compute the absolute resolved path using path.resolve(ROOT_DIR, userInputPath).
  • Optionally, call fs.realpathSync for sym-link safety.
  • Check that the result starts with the root directory's absolute path (resolvedPath.startsWith(ROOT_DIR)).
  • If it does not, throw an error or return an appropriate response.

You will need to:

  • Define ROOT_DIR (at the top of the module).
  • For each handler, wrap the critical path(s) with normalization and boundary check.
  • For functions accessing parent directories (such as write-file, which creates parent directory), ensure that the computed parent is also checked under the same boundary.

In terms of code regions:

  • Insert a const ROOT_DIR = ... (e.g., /pulse-editor).
  • In all relevant handler functions (handleWriteFile, handleReadFile, handleDelete, handleCopyFiles, handleHasPath), replace direct use of the URIs/paths with the safe, normalized, boundary-checked path.

Suggested changeset 1
remote-workspace/src/servers/api-server/platform-api/handler.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/remote-workspace/src/servers/api-server/platform-api/handler.ts b/remote-workspace/src/servers/api-server/platform-api/handler.ts
--- a/remote-workspace/src/servers/api-server/platform-api/handler.ts
+++ b/remote-workspace/src/servers/api-server/platform-api/handler.ts
@@ -3,8 +3,9 @@
 import path from "path";
 
 // Define a safe root directory for projects. Can be overridden by env or configured as needed.
+const ROOT_DIR = "/pulse-editor";
 
-const settingsPath = path.join("/pulse-editor", "settings.json");
+const settingsPath = path.join(ROOT_DIR, "settings.json");
 
 export async function handlePlatformAPIRequest(
   data: {
@@ -225,20 +225,27 @@
 }
 
 async function handleDelete(uri: string) {
-  await fs.promises.rm(uri, {
+  const safePath = getSafePath(uri);
+  await fs.promises.rm(safePath, {
     recursive: true,
     force: true,
   });
 }
 
 async function handleHasPath(uri: string) {
-  return fs.existsSync(uri);
+  try {
+    const safePath = getSafePath(uri);
+    return fs.existsSync(safePath);
+  } catch {
+    return false;
+  }
 }
 
 async function handleReadFile(uri: string) {
   // Read the file at validated path
+  const safePath = getSafePath(uri);
   const data = await fs.promises.readFile(
-    uri,
+    safePath,
     "utf-8",
   );
 
@@ -247,9 +243,12 @@
 
 async function handleWriteFile(data: any, uri: string) {
   // Write the data at validated path
-  const safePath = uri;
+  const safePath = getSafePath(uri);
   // create parent directory if it doesn't exist
   const dir = path.dirname(safePath);
+  if (!dir.startsWith(ROOT_DIR)) {
+    throw new Error("Access to parent directory outside of project root is not allowed.");
+  }
   if (!fs.existsSync(dir)) {
     fs.mkdirSync(dir, { recursive: true });
   }
@@ -259,9 +256,11 @@
 
 async function handleCopyFiles(from: string, to: string) {
   // Copy the files from the validated from path to the validated to path
+  const safeFrom = getSafePath(from);
+  const safeTo = getSafePath(to);
   await fs.promises.cp(
-    from,
-    to,
+    safeFrom,
+    safeTo,
     {
       recursive: true,
     },
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
fs.mkdirSync(dir, { recursive: true });
}

fs.writeFileSync(safePath, data);

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 17 days ago

To fix the uncontrolled path usage, all user-supplied paths must be validated and restricted to a safe location. The best approach is as follows:

  • Define a constant (e.g., PROJECTS_ROOT) as the root directory where all permitted file operations take place (e.g., /pulse-editor/projects).
  • Replace all raw usage of the uri argument in file access functions (handleWriteFile, handleReadFile, handleDelete, handleHasPath, handleCopyFiles) by resolving the final path using path.resolve(PROJECTS_ROOT, uri) to eliminate any .. segments and to convert to an absolute path.
  • After resolution, ensure the path is strictly contained in PROJECTS_ROOT using startsWith or similar.
  • Deny access (throw error or return a clear response) if the resolved path is not a subdirectory or file within the designated root.
  • Optionally, use filenames sanitization (sanitize-filename) if you want to restrict to simple filenames, but using the "root containment" technique works for (almost) all use-cases if you intend to allow directories/files underneath a given root.

Changes will affect remote-workspace/src/servers/api-server/platform-api/handler.ts in the relevant handler functions. The only necessary import is path (already present), so no third-party libraries are strictly required for root containment. There is no need to edit remote-workspace/src/servers/api-server/index.ts, except to ensure the handler is still called correctly.


Suggested changeset 1
remote-workspace/src/servers/api-server/platform-api/handler.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/remote-workspace/src/servers/api-server/platform-api/handler.ts b/remote-workspace/src/servers/api-server/platform-api/handler.ts
--- a/remote-workspace/src/servers/api-server/platform-api/handler.ts
+++ b/remote-workspace/src/servers/api-server/platform-api/handler.ts
@@ -4,6 +4,7 @@
 
 // Define a safe root directory for projects. Can be overridden by env or configured as needed.
 
+const PROJECTS_ROOT = "/pulse-editor/projects"; // Change this as required
 const settingsPath = path.join("/pulse-editor", "settings.json");
 
 export async function handlePlatformAPIRequest(
@@ -225,20 +226,32 @@
 }
 
 async function handleDelete(uri: string) {
-  await fs.promises.rm(uri, {
+  const targetPath = path.resolve(PROJECTS_ROOT, uri);
+  if (!targetPath.startsWith(PROJECTS_ROOT)) {
+    throw new Error("Access denied: Path outside allowed root");
+  }
+  await fs.promises.rm(targetPath, {
     recursive: true,
     force: true,
   });
 }
 
 async function handleHasPath(uri: string) {
-  return fs.existsSync(uri);
+  const targetPath = path.resolve(PROJECTS_ROOT, uri);
+  if (!targetPath.startsWith(PROJECTS_ROOT)) {
+    return false;
+  }
+  return fs.existsSync(targetPath);
 }
 
 async function handleReadFile(uri: string) {
+  const targetPath = path.resolve(PROJECTS_ROOT, uri);
+  if (!targetPath.startsWith(PROJECTS_ROOT)) {
+    throw new Error("Access denied: Path outside allowed root");
+  }
   // Read the file at validated path
   const data = await fs.promises.readFile(
-    uri,
+    targetPath,
     "utf-8",
   );
 
@@ -246,8 +248,10 @@
 }
 
 async function handleWriteFile(data: any, uri: string) {
-  // Write the data at validated path
-  const safePath = uri;
+  const safePath = path.resolve(PROJECTS_ROOT, uri);
+  if (!safePath.startsWith(PROJECTS_ROOT)) {
+    throw new Error("Access denied: Path outside allowed root");
+  }
   // create parent directory if it doesn't exist
   const dir = path.dirname(safePath);
   if (!fs.existsSync(dir)) {
@@ -258,10 +262,15 @@
 }
 
 async function handleCopyFiles(from: string, to: string) {
+  const fromPath = path.resolve(PROJECTS_ROOT, from);
+  const toPath = path.resolve(PROJECTS_ROOT, to);
+  if (!fromPath.startsWith(PROJECTS_ROOT) || !toPath.startsWith(PROJECTS_ROOT)) {
+    throw new Error("Access denied: Path outside allowed root");
+  }
   // Copy the files from the validated from path to the validated to path
   await fs.promises.cp(
-    from,
-    to,
+    fromPath,
+    toPath,
     {
       recursive: true,
     },
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
async function handleCopyFiles(from: string, to: string) {
// Copy the files from the validated from path to the validated to path
await fs.promises.cp(
from,

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 17 days ago

The best fix is to restrict file/directory operations to a safe root directory (e.g., /pulse-editor/projects or similar). For each handler function accepting a path from untrusted input (not just copy-files, but also delete, write-file, etc.), normalize the input path relative to the root and check that the resolved absolute path indeed lives within (is a descendant of) that root. This means:

  • Normalize using path.resolve and/or fs.realpathSync (for symlink attacks protection).
  • For each relevant function, change the usage from directly trusting from, to, uri, etc. to mapping them to validated, normalized safe paths.
  • Add a reusable helper (e.g., getSafePath(subPath: string)), which throws/rejects if the resolved path is not within the root.
  • Use this helper in all affected functions.
  • Update handleCopyFiles to use safe paths for both from and to.

The edits should be in remote-workspace/src/servers/api-server/platform-api/handler.ts only, and must not alter functional logic, just add validation.

You only need to add imports from well-known external libraries. However, for normalization and checking, standard Node.js path and fs suffice.


Suggested changeset 1
remote-workspace/src/servers/api-server/platform-api/handler.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/remote-workspace/src/servers/api-server/platform-api/handler.ts b/remote-workspace/src/servers/api-server/platform-api/handler.ts
--- a/remote-workspace/src/servers/api-server/platform-api/handler.ts
+++ b/remote-workspace/src/servers/api-server/platform-api/handler.ts
@@ -4,8 +4,28 @@
 
 // Define a safe root directory for projects. Can be overridden by env or configured as needed.
 
+const ROOT = "/pulse-editor/projects";
 const settingsPath = path.join("/pulse-editor", "settings.json");
 
+// Helper to resolve and validate a user-provided path within the root directory
+function getSafePath(subPath: string): string {
+  const resolved = path.resolve(ROOT, subPath);
+  // Optionally further resolve via fs.realpathSync to prevent symlink attacks
+  let realResolved;
+  try {
+    realResolved = fs.realpathSync(resolved);
+  } catch {
+    // If path does not exist yet (for creation), fallback to resolved
+    realResolved = resolved;
+  }
+  // Always ensure trailing slash on ROOT to avoid partial-match attacks
+  const rootWithSlash = path.resolve(ROOT) + path.sep;
+  if (!realResolved.startsWith(rootWithSlash)) {
+    throw new Error("Path outside of workspace root is not allowed");
+  }
+  return realResolved;
+}
+
 export async function handlePlatformAPIRequest(
   data: {
     operation: string;
@@ -225,20 +243,27 @@
 }
 
 async function handleDelete(uri: string) {
-  await fs.promises.rm(uri, {
+  const safePath = getSafePath(uri);
+  await fs.promises.rm(safePath, {
     recursive: true,
     force: true,
   });
 }
 
 async function handleHasPath(uri: string) {
-  return fs.existsSync(uri);
+  try {
+    const safePath = getSafePath(uri);
+    return fs.existsSync(safePath);
+  } catch {
+    return false;
+  }
 }
 
 async function handleReadFile(uri: string) {
+  const safePath = getSafePath(uri);
   // Read the file at validated path
   const data = await fs.promises.readFile(
-    uri,
+    safePath,
     "utf-8",
   );
 
@@ -247,7 +261,7 @@
 
 async function handleWriteFile(data: any, uri: string) {
   // Write the data at validated path
-  const safePath = uri;
+  const safePath = getSafePath(uri);
   // create parent directory if it doesn't exist
   const dir = path.dirname(safePath);
   if (!fs.existsSync(dir)) {
@@ -259,9 +273,11 @@
 
 async function handleCopyFiles(from: string, to: string) {
   // Copy the files from the validated from path to the validated to path
+  const safeFrom = getSafePath(from);
+  const safeTo = getSafePath(to);
   await fs.promises.cp(
-    from,
-    to,
+    safeFrom,
+    safeTo,
     {
       recursive: true,
     },
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
// Copy the files from the validated from path to the validated to path
await fs.promises.cp(
from,
to,

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 17 days ago

To prevent path traversal vulnerabilities, we should restrict file operations to a "safe root directory." Every user-provided file path (including both from and to in copy operations) must be resolved and normalized relative to this root, then checked to ensure the result stays within the root directory. This typically involves using path.resolve to join the root with the user-provided path, then confirming the resulting path starts with the root directory. If this check fails, the operation should throw an error.

We'll make the following changes:

  • Choose a secure, absolute root directory (e.g., /pulse-editor as already used elsewhere in the file).
  • Update handleCopyFiles to:
    • Resolve both from and to against the root directory using path.resolve.
    • (Optional, but good practice) Ensure the actual realpath exists within the root using fs.realpathSync.
    • Check both resolved paths start with the root directory.
    • If either path does not, throw an error or return a 403/permission denied.
  • Optionally, for consistency/safety, wrap this path-validation logic in a helper method.

Code changes needed:

  • In handler.ts:
    • Define a constant for the safe root directory if not already present.
    • Add a helper function (e.g., resolveSafePath) that validates paths against this root.
    • Update handleCopyFiles to call this helper for both from and to.
    • Raise an error (e.g., throw new Error("Access denied")) if the check fails.

Suggested changeset 1
remote-workspace/src/servers/api-server/platform-api/handler.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/remote-workspace/src/servers/api-server/platform-api/handler.ts b/remote-workspace/src/servers/api-server/platform-api/handler.ts
--- a/remote-workspace/src/servers/api-server/platform-api/handler.ts
+++ b/remote-workspace/src/servers/api-server/platform-api/handler.ts
@@ -4,8 +4,11 @@
 
 // Define a safe root directory for projects. Can be overridden by env or configured as needed.
 
-const settingsPath = path.join("/pulse-editor", "settings.json");
+// Define a safe root directory. Should be an absolute path. Consider making this configurable via env.
+const SAFE_ROOT = "/pulse-editor";
 
+const settingsPath = path.join(SAFE_ROOT, "settings.json");
+
 export async function handlePlatformAPIRequest(
   data: {
     operation: string;
@@ -257,11 +259,23 @@
   fs.writeFileSync(safePath, data);
 }
 
+function resolveSafePath(userPath: string): string {
+  // Resolve the path relative to the root
+  const absPath = path.resolve(SAFE_ROOT, userPath);
+  // Ensure the resulting path is within the SAFE_ROOT
+  if (!absPath.startsWith(SAFE_ROOT + path.sep)) {
+    throw new Error("Access denied: Path outside of safe root directory");
+  }
+  return absPath;
+}
+
 async function handleCopyFiles(from: string, to: string) {
-  // Copy the files from the validated from path to the validated to path
+  // Validate and resolve the supplied paths
+  const safeFrom = resolveSafePath(from);
+  const safeTo = resolveSafePath(to);
   await fs.promises.cp(
-    from,
-    to,
+    safeFrom,
+    safeTo,
     {
       recursive: true,
     },
EOF
@@ -4,8 +4,11 @@

// Define a safe root directory for projects. Can be overridden by env or configured as needed.

const settingsPath = path.join("/pulse-editor", "settings.json");
// Define a safe root directory. Should be an absolute path. Consider making this configurable via env.
const SAFE_ROOT = "/pulse-editor";

const settingsPath = path.join(SAFE_ROOT, "settings.json");

export async function handlePlatformAPIRequest(
data: {
operation: string;
@@ -257,11 +259,23 @@
fs.writeFileSync(safePath, data);
}

function resolveSafePath(userPath: string): string {
// Resolve the path relative to the root
const absPath = path.resolve(SAFE_ROOT, userPath);
// Ensure the resulting path is within the SAFE_ROOT
if (!absPath.startsWith(SAFE_ROOT + path.sep)) {
throw new Error("Access denied: Path outside of safe root directory");
}
return absPath;
}

async function handleCopyFiles(from: string, to: string) {
// Copy the files from the validated from path to the validated to path
// Validate and resolve the supplied paths
const safeFrom = resolveSafePath(from);
const safeTo = resolveSafePath(to);
await fs.promises.cp(
from,
to,
safeFrom,
safeTo,
{
recursive: true,
},
Copilot is powered by AI and may make mistakes. Always verify output.
@Shellishack Shellishack merged commit e02eb1b into main Oct 29, 2025
11 of 12 checks passed
@Shellishack Shellishack deleted the Support-remote-workspace branch October 29, 2025 13:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants