{"record":{"id":"c9c54f532bef651c","repo":"schollz/croc","slug":"remote-path-must-be-relative-value","errorCode":null,"errorMessage":"Remote path must be relative: ${value}","messagePattern":"Remote path must be relative: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/src/protocol/metadata.ts","lineNumber":26,"sourceCode":"\nfunction cleanSegments(value: string) {\n  const replaced = value.replaceAll(\"\\\\\", \"/\");\n  if (replaced.includes(\"\\0\")) throw new Error(\"A remote path contains a null byte\");\n  const segments: string[] = [];\n  for (const segment of replaced.split(\"/\")) {\n    if (segment === \"\" || segment === \".\") continue;\n    if (segment === \"..\") throw new Error(`Remote path escapes the destination: ${value}`);\n    if ([...segment].some((character) => !/\\P{C}/u.test(character))) {\n      throw new Error(`Remote path contains a non-printable character: ${value}`);\n    }\n    segments.push(segment);\n  }\n  return segments;\n}\n\nexport function normalizeFolder(value = \".\") {\n  if (/^(?:[a-zA-Z]:|\\/)/.test(value)) {\n    throw new Error(`Remote path must be relative: ${value}`);\n  }\n  const segments = cleanSegments(value);\n  const normalized = segments.join(\"/\") || \".\";\n  if (normalized.includes(\".ssh\")) {\n    throw new Error(`Remote path is not allowed: ${value}`);\n  }\n  return normalized;\n}\n\nexport function normalizeFilePath(folderValue: string, nameValue: string) {\n  const folder = normalizeFolder(folderValue);\n  const nameSegments = cleanSegments(nameValue);\n  if (nameSegments.length !== 1 || nameSegments[0] !== nameValue.replaceAll(\"\\\\\", \"/\")) {\n    throw new Error(`Remote filename must be a basename: ${nameValue}`);\n  }\n  const name = nameSegments[0];\n  if (!name) throw new Error(\"Remote filename is empty\");\n  const path = folder === \".\" ? name : `${folder}/${name}`;","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/schollz/croc/blob/e25f1bdc04f07f094d50b0a1bf67e2563944b57a/web/src/protocol/metadata.ts#L8-L44","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Pass only relative folder strings ('sub/dir' or '.') into normalizeFolder; keep the absolute download root as a separate config value applied at write time.","Strip or reject leading slashes and drive letters on the sender before building the offer.","Refuse transfers whose offers carry absolute destinations — treat them as malformed or hostile metadata."],"exampleFix":"// before\nconst folder = normalizeFolder(config.downloadDir); // '/home/me/downloads'\n// after\nconst folder = normalizeFolder(\".\"); // offer-relative folder; apply downloadDir at save time","handlingStrategy":"validation","validationCode":"function isRelativeRemotePath(value: string): boolean {\n  return !/^(?:[a-zA-Z]:|\\/)/.test(value);\n}\nif (!isRelativeRemotePath(folderValue)) throw new TypeError(\"absolute destination not allowed\");","typeGuard":null,"tryCatchPattern":"try {\n  const { folder } = normalizeFolder(candidate);\n} catch (error) {\n  if (error instanceof Error && error.message.startsWith(\"Remote path must be relative\")) {\n    // strip the root and retry with the relative remainder, or reject the offer\n    rejectOffer(`absolute folder not allowed: ${candidate}`);\n    return;\n  }\n  throw error;\n}","preventionTips":["Keep the absolute download root in receiver config; never pass it through normalizeFolder.","Send only relative folder strings in offers ('sub/dir', '.').","Reject offers with drive-letter or leading-slash destinations."],"tags":["security","path-validation","metadata","windows"],"backgroundTag":null,"analyzedSha":"e25f1bdc04f07f094d50b0a1bf67e2563944b57a","analyzedAt":"2026-08-15T12:53:39.096Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}