#4453·composio

RemoteFile.save() (TS) crashes with unhandled EISDIR on malformed mountRelativePath; Python's SEC-316 fix never ported

Author: VANDRANKICreated Sep 11, 2026Updated Sep 11, 2026
Labelsbugts

Bug Description

RemoteFile.save() in the TypeScript SDK (ts/packages/core/src/models/RemoteFile.ts, lines 167-194) is missing a path-validation fix that the Python SDK already has (tracked there as SEC-316), so a malformed mount_relative_path value from the API response crashes the save with an unhandled EISDIR instead of a clean error.

typescript
get filename(): string {
  return platform.basename(this.mountRelativePath);
}

async save(path?: string): Promise<string> {
  ...
  const savePath =
    path ?? platform.joinPath(homeDir, COMPOSIO_DIR, TEMP_FILES_DIRECTORY_NAME, this.filename);
  const dir =
    path != null ? getParentDir(savePath) : platform.joinPath(homeDir, COMPOSIO_DIR, TEMP_FILES_DIRECTORY_NAME);
  if (dir && !platform.existsSync(dir)) {
    platform.mkdirSync(dir);
  }
  platform.writeFileSync(savePath, content);
  return savePath;
}

platform.basename is a bare path.basename(filePath) with no validation, and mount_relative_path (ts/packages/core/src/types/ToolRouterSessionFilesMount.types.ts) is declared as a plain z.string() with no non-empty constraint - a direct passthrough of an untrusted API response field.

The Python SDK's RemoteFile.save() (python/composio/core/models/tool_router_session_files.py) calls secure_basename_join(), whose safe_basename() helper (python/composio/utils/safe_path.py) explicitly documents and rejects this exact case:

"Names that leave no usable basename are refused rather than replaced with a generated one: a response that cannot name its own file is malformed or hostile... . and the empty string both basename to "", which makes an output path equal to its own directory and surfaces as IsADirectoryError at write time."

The TS port never received this fix.

Steps to Reproduce

javascript
const path = require('path');
const os = require('os');
const homeDir = os.homedir();

for (const mountRelativePath of ['', '.', 'sub/.']) {
  const filename = path.basename(mountRelativePath);          // '' or '.'
  const savePath = path.join(homeDir, '.composio', 'files', filename);
  const dir      = path.join(homeDir, '.composio', 'files');
  console.log(mountRelativePath, '-> savePath === dir:', savePath === dir); // true for all three
}

Confirmed by running the above (all three collapse savePath to the containing directory), and separately confirmed fs.writeFileSync(existingDirPath, buffer) throws EISDIR: illegal operation on a directory, open '<dir>'.

Any RemoteFile whose mountRelativePath is "", ".", or ends in /. - obtainable from a malformed/unexpected backend response, or from a caller's own download('.')-style call whose value the API echoes back - makes remoteFile.save() (called without an explicit path) crash with an unhandled EISDIR instead of a validation error. Note path.basename() does strip directory separators, so this is a crash/robustness gap rather than a path-traversal write outside the target directory.

Fix direction

Port the Python SDK's safe_basename/secure_basename_join behavior (or an equivalent minimal check) to the TS RemoteFile.save(): reject mountRelativePath values that basename to "" or "." with a clear SDK error instead of letting fs.writeFileSync fail with a raw EISDIR.

Additional Information

Verified on next branch. Duplicate-checked (RemoteFile+save+EISDIR, mountRelativePath+basename, SEC-316, RemoteFile.save, secure_basename_join, path+traversal, IsADirectoryError, RemoteFile - all 0 or unrelated results; the one RemoteFile hit, #3560, is a different closed bug about a missing HTTP timeout in buffer()).