Ensure fs changes in response processor are within app dir (#496)
This commit is contained in:
@@ -5,6 +5,7 @@ import fs from "node:fs";
|
||||
import { getDyadAppPath } from "../../paths/paths";
|
||||
import path from "node:path";
|
||||
import git from "isomorphic-git";
|
||||
import { safeJoin } from "../utils/path_utils";
|
||||
|
||||
import log from "electron-log";
|
||||
import { executeAddDependency } from "./executeAddDependency";
|
||||
@@ -296,11 +297,11 @@ export async function processFullResponseActions(
|
||||
}
|
||||
writtenFiles.push("package.json");
|
||||
const pnpmFilename = "pnpm-lock.yaml";
|
||||
if (fs.existsSync(path.join(appPath, pnpmFilename))) {
|
||||
if (fs.existsSync(safeJoin(appPath, pnpmFilename))) {
|
||||
writtenFiles.push(pnpmFilename);
|
||||
}
|
||||
const packageLockFilename = "package-lock.json";
|
||||
if (fs.existsSync(path.join(appPath, packageLockFilename))) {
|
||||
if (fs.existsSync(safeJoin(appPath, packageLockFilename))) {
|
||||
writtenFiles.push(packageLockFilename);
|
||||
}
|
||||
}
|
||||
@@ -319,7 +320,7 @@ export async function processFullResponseActions(
|
||||
|
||||
// Process all file deletions
|
||||
for (const filePath of dyadDeletePaths) {
|
||||
const fullFilePath = path.join(appPath, filePath);
|
||||
const fullFilePath = safeJoin(appPath, filePath);
|
||||
|
||||
// Delete the file if it exists
|
||||
if (fs.existsSync(fullFilePath)) {
|
||||
@@ -362,8 +363,8 @@ export async function processFullResponseActions(
|
||||
|
||||
// Process all file renames
|
||||
for (const tag of dyadRenameTags) {
|
||||
const fromPath = path.join(appPath, tag.from);
|
||||
const toPath = path.join(appPath, tag.to);
|
||||
const fromPath = safeJoin(appPath, tag.from);
|
||||
const toPath = safeJoin(appPath, tag.to);
|
||||
|
||||
// Ensure target directory exists
|
||||
const dirPath = path.dirname(toPath);
|
||||
@@ -427,7 +428,7 @@ export async function processFullResponseActions(
|
||||
for (const tag of dyadWriteTags) {
|
||||
const filePath = tag.path;
|
||||
const content = tag.content;
|
||||
const fullFilePath = path.join(appPath, filePath);
|
||||
const fullFilePath = safeJoin(appPath, filePath);
|
||||
|
||||
// Ensure directory exists
|
||||
const dirPath = path.dirname(fullFilePath);
|
||||
|
||||
60
src/ipc/utils/path_utils.ts
Normal file
60
src/ipc/utils/path_utils.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import path from "node:path";
|
||||
|
||||
/**
|
||||
* Safely joins paths while ensuring the result stays within the base directory.
|
||||
* This prevents directory traversal attacks where malicious paths like "../../etc/passwd"
|
||||
* could be used to access files outside the intended directory.
|
||||
*
|
||||
* @param basePath The base directory that should contain the result
|
||||
* @param ...paths Path segments to join with the base path
|
||||
* @returns The joined path if it's within the base directory
|
||||
* @throws Error if the resulting path would be outside the base directory
|
||||
*/
|
||||
export function safeJoin(basePath: string, ...paths: string[]): string {
|
||||
// Check if any of the path segments are absolute paths (which would be unsafe)
|
||||
for (const pathSegment of paths) {
|
||||
if (path.isAbsolute(pathSegment)) {
|
||||
throw new Error(
|
||||
`Unsafe path: joining "${paths.join(", ")}" with base "${basePath}" would escape the base directory`,
|
||||
);
|
||||
}
|
||||
// Also check for home directory shortcuts which are effectively absolute
|
||||
if (pathSegment.startsWith("~/")) {
|
||||
throw new Error(
|
||||
`Unsafe path: joining "${paths.join(", ")}" with base "${basePath}" would escape the base directory`,
|
||||
);
|
||||
}
|
||||
// Check for Windows-style absolute paths (C:\, D:\, etc.)
|
||||
if (/^[A-Za-z]:[/\\]/.test(pathSegment)) {
|
||||
throw new Error(
|
||||
`Unsafe path: joining "${paths.join(", ")}" with base "${basePath}" would escape the base directory`,
|
||||
);
|
||||
}
|
||||
// Check for UNC paths (\\server\share)
|
||||
if (pathSegment.startsWith("\\\\")) {
|
||||
throw new Error(
|
||||
`Unsafe path: joining "${paths.join(", ")}" with base "${basePath}" would escape the base directory`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Join all the paths
|
||||
const joinedPath = path.join(basePath, ...paths);
|
||||
|
||||
// Resolve both paths to absolute paths to handle any ".." components
|
||||
const resolvedBasePath = path.resolve(basePath);
|
||||
const resolvedJoinedPath = path.resolve(joinedPath);
|
||||
|
||||
// Check if the resolved joined path starts with the base path
|
||||
// Use path.relative to ensure we're doing a proper path comparison
|
||||
const relativePath = path.relative(resolvedBasePath, resolvedJoinedPath);
|
||||
|
||||
// If relativePath starts with ".." or is absolute, then resolvedJoinedPath is outside basePath
|
||||
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
||||
throw new Error(
|
||||
`Unsafe path: joining "${paths.join(", ")}" with base "${basePath}" would escape the base directory`,
|
||||
);
|
||||
}
|
||||
|
||||
return joinedPath;
|
||||
}
|
||||
Reference in New Issue
Block a user