Skip to content

Commit e26ba5a

Browse files
committed
Fix uncontrolled data used in path expression in remote workspace
1 parent 1453ba0 commit e26ba5a

File tree

1 file changed

+55
-28
lines changed
  • remote-workspace/src/servers/api-server/platform-api

1 file changed

+55
-28
lines changed

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

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,29 @@ import ignore from "ignore";
33
import path from "path";
44

55
// Define a safe root directory for projects. Can be overridden by env or configured as needed.
6+
// All incoming URIs will be resolved and validated to ensure they don't escape this root.
7+
const SAFE_ROOT = path.resolve(process.env.PLATFORM_API_ROOT ?? "/pulse-editor");
68

7-
const settingsPath = path.join("/pulse-editor", "settings.json");
9+
const settingsPath = path.join(SAFE_ROOT, "settings.json");
10+
11+
function safeResolve(uri: string): string {
12+
if (!uri || typeof uri !== "string") {
13+
throw new Error("Invalid path");
14+
}
15+
16+
// Resolve the input and the safe root to absolute, normalized paths.
17+
const resolved = path.resolve(uri);
18+
const root = SAFE_ROOT;
19+
20+
const relative = path.relative(root, resolved);
21+
22+
// If the relative path starts with '..' or is absolute, it escapes the SAFE_ROOT.
23+
if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
24+
return resolved;
25+
}
26+
27+
throw new Error("Can only access paths within the project home directory.");
28+
}
829

930
export async function handlePlatformAPIRequest(
1031
data: {
@@ -113,7 +134,7 @@ export async function handlePlatformAPIRequest(
113134

114135
// List all folders in a path
115136
async function handleListProjects(uri: string) {
116-
const rootPath = uri;
137+
const rootPath = safeResolve(uri);
117138
const files = await fs.promises.readdir(rootPath, { withFileTypes: true });
118139
const folders = files
119140
.filter((file) => file.isDirectory())
@@ -131,7 +152,7 @@ async function listPathContent(
131152
options: any,
132153
baseUri: string | undefined = undefined,
133154
) {
134-
const rootPath = uri;
155+
const rootPath = safeResolve(uri);
135156
const files = await fs.promises.readdir(rootPath, { withFileTypes: true });
136157

137158
const promise: Promise<any>[] = files
@@ -188,12 +209,14 @@ async function handleListPathContent(uri: string, options: any) {
188209

189210
async function handleCreateProject(uri: string) {
190211
// Create a folder at the validated path
191-
await fs.promises.mkdir(uri);
212+
const safe = safeResolve(uri);
213+
await fs.promises.mkdir(safe, { recursive: true });
192214
}
193215

194216
async function handleDeleteProject(uri: string) {
195217
// Delete the folder at the validated path
196-
await fs.promises.rm(uri, { recursive: true, force: true });
218+
const safe = safeResolve(uri);
219+
await fs.promises.rm(safe, { recursive: true, force: true });
197220
}
198221

199222
async function handleUpdateProject(
@@ -203,51 +226,59 @@ async function handleUpdateProject(
203226
ctime?: Date;
204227
},
205228
) {
206-
const newUri = path.join(path.dirname(uri), updatedInfo.name);
207-
await fs.promises.rename(uri, newUri);
229+
const safeOld = safeResolve(uri);
230+
const newPathCandidate = path.join(path.dirname(safeOld), updatedInfo.name);
231+
const safeNew = safeResolve(newPathCandidate);
232+
await fs.promises.rename(safeOld, safeNew);
208233
}
209234

210235
async function handleCreateFolder(uri: string) {
211236
// Create a folder at the validated path
212-
await fs.promises.mkdir(uri);
237+
const safe = safeResolve(uri);
238+
await fs.promises.mkdir(safe, { recursive: true });
213239
}
214240

215241
async function handleCreateFile(uri: string) {
216242
// Create a file at the validated path
217-
await fs.promises.writeFile(uri, "");
243+
const safe = safeResolve(uri);
244+
// ensure parent exists
245+
await fs.promises.mkdir(path.dirname(safe), { recursive: true });
246+
await fs.promises.writeFile(safe, "");
218247
}
219248

220249
async function handleRename(oldUri: string, newUri: string) {
221-
await fs.promises.rename(
222-
oldUri,
223-
newUri,
224-
);
250+
const safeOld = safeResolve(oldUri);
251+
const safeNew = safeResolve(newUri);
252+
await fs.promises.rename(safeOld, safeNew);
225253
}
226254

227255
async function handleDelete(uri: string) {
228-
await fs.promises.rm(uri, {
256+
const safe = safeResolve(uri);
257+
await fs.promises.rm(safe, {
229258
recursive: true,
230259
force: true,
231260
});
232261
}
233262

234263
async function handleHasPath(uri: string) {
235-
return fs.existsSync(uri);
264+
try {
265+
const safe = safeResolve(uri);
266+
return fs.existsSync(safe);
267+
} catch (err) {
268+
return false;
269+
}
236270
}
237271

238272
async function handleReadFile(uri: string) {
239273
// Read the file at validated path
240-
const data = await fs.promises.readFile(
241-
uri,
242-
"utf-8",
243-
);
244-
274+
const safe = safeResolve(uri);
275+
const data = await fs.promises.readFile(safe, "utf-8");
245276
return data;
246277
}
247278

248279
async function handleWriteFile(data: any, uri: string) {
249280
// Write the data at validated path
250-
const safePath = uri;
281+
const safePath = safeResolve(uri);
251282
// create parent directory if it doesn't exist
252283
const dir = path.dirname(safePath);
253284
if (!fs.existsSync(dir)) {
@@ -259,13 +290,9 @@ async function handleWriteFile(data: any, uri: string) {
259290

260291
async function handleCopyFiles(from: string, to: string) {
261292
// Copy the files from the validated from path to the validated to path
262-
await fs.promises.cp(
263-
from,
264-
to,
265-
{
266-
recursive: true,
267-
},
268-
);
293+
const safeFrom = safeResolve(from);
294+
const safeTo = safeResolve(to);
295+
await fs.promises.cp(safeFrom, safeTo, { recursive: true });
269296
}
270297

271298
async function handleLoadSettings() {

0 commit comments

Comments
 (0)