schollz/croc · error · Error

Remote path must be relative: ${value}

Error message

Remote path must be relative: ${value}

What it means

normalizeFolder() requires every remote folder to be a relative path: the regex /^(?:[a-zA-Z]:|\/)/ rejects POSIX absolute paths (leading '/') and Windows drive letters ('C:', 'D:\...'). Destination folders are always interpreted inside the receiver-chosen download root, so absolute destinations are meaningless and dangerous.

Source

Thrown at web/src/protocol/metadata.ts:26

function cleanSegments(value: string) {
  const replaced = value.replaceAll("\\", "/");
  if (replaced.includes("\0")) throw new Error("A remote path contains a null byte");
  const segments: string[] = [];
  for (const segment of replaced.split("/")) {
    if (segment === "" || segment === ".") continue;
    if (segment === "..") throw new Error(`Remote path escapes the destination: ${value}`);
    if ([...segment].some((character) => !/\P{C}/u.test(character))) {
      throw new Error(`Remote path contains a non-printable character: ${value}`);
    }
    segments.push(segment);
  }
  return segments;
}

export function normalizeFolder(value = ".") {
  if (/^(?:[a-zA-Z]:|\/)/.test(value)) {
    throw new Error(`Remote path must be relative: ${value}`);
  }
  const segments = cleanSegments(value);
  const normalized = segments.join("/") || ".";
  if (normalized.includes(".ssh")) {
    throw new Error(`Remote path is not allowed: ${value}`);
  }
  return normalized;
}

export function normalizeFilePath(folderValue: string, nameValue: string) {
  const folder = normalizeFolder(folderValue);
  const nameSegments = cleanSegments(nameValue);
  if (nameSegments.length !== 1 || nameSegments[0] !== nameValue.replaceAll("\\", "/")) {
    throw new Error(`Remote filename must be a basename: ${nameValue}`);
  }
  const name = nameSegments[0];
  if (!name) throw new Error("Remote filename is empty");
  const path = folder === "." ? name : `${folder}/${name}`;

View on GitHub (pinned to e25f1bdc04)

Solutions

  1. Pass only relative folder strings ('sub/dir' or '.') into normalizeFolder; keep the absolute download root as a separate config value applied at write time.
  2. Strip or reject leading slashes and drive letters on the sender before building the offer.
  3. Refuse transfers whose offers carry absolute destinations — treat them as malformed or hostile metadata.

Example fix

// before
const folder = normalizeFolder(config.downloadDir); // '/home/me/downloads'
// after
const folder = normalizeFolder("."); // offer-relative folder; apply downloadDir at save time
Defensive patterns

Strategy: validation

Validate before calling

function isRelativeRemotePath(value: string): boolean {
  return !/^(?:[a-zA-Z]:|\/)/.test(value);
}
if (!isRelativeRemotePath(folderValue)) throw new TypeError("absolute destination not allowed");

Try / catch

try {
  const { folder } = normalizeFolder(candidate);
} catch (error) {
  if (error instanceof Error && error.message.startsWith("Remote path must be relative")) {
    // strip the root and retry with the relative remainder, or reject the offer
    rejectOffer(`absolute folder not allowed: ${candidate}`);
    return;
  }
  throw error;
}

Prevention

When it happens

Trigger: validateSenderInfo() receives an offer with fr='/tmp/evil' or fr='C:\Users\x' (backslashes are checked after replacement too — 'C:/' still matches the drive-letter branch), or your own code calls normalizeFolder with an absolute path.

Common situations: A hostile peer trying to pin an absolute destination; a sender on Windows passing drive-qualified folders through unnormalized; receiver-side code that forwards user config (download dir) into normalizeFolder instead of using it only as a root.

Related errors


AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15). Data as JSON: /api/errors/c9c54f532bef651c. Report an issue: GitHub.