{"record":{"id":"00c5ac3f3a4cad1c","repo":"schollz/croc","slug":"remote-path-escapes-the-destination-value","errorCode":null,"errorMessage":"Remote path escapes the destination: ${value}","messagePattern":"Remote path escapes the destination: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/src/protocol/metadata.ts","lineNumber":15,"sourceCode":"import { base64ToBytes } from \"./bytes\";\nimport type {\n  OfferedFile,\n  SenderInfoWire,\n  TransferOffer,\n  WireFileInfo,\n} from \"./types\";\n\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;","sourceCodeStart":1,"sourceCodeEnd":33,"githubUrl":"https://github.com/schollz/croc/blob/e25f1bdc04f07f094d50b0a1bf67e2563944b57a/web/src/protocol/metadata.ts#L1-L33","documentation":"cleanSegments() splits a remote path on '/' (after converting backslashes) and throws if any segment is '..'. This blocks path-traversal: a peer-supplied path like '../../etc/passwd' must never escape the destination directory chosen by the receiver.","triggerScenarios":"validateSenderInfo() processes an offer whose file folder/name or empty-folder entry contains a '..' segment after backslash normalization — e.g. fr='docs/../..' or n='a/../../x'.","commonSituations":"Hostile sender attempting directory traversal against the receiver's download directory; fuzz corpus containing traversal payloads; legitimate sender whose folder strings were built with '..' segments that were never normalized on the Go side (rare — croc normalizes before sending).","solutions":["Refuse the transfer from the peer that sent the traversal payload; this is a security guard, not a recoverable condition.","If you are sending, normalize folders to clean relative paths ('a/b', not 'a/../b') before constructing SenderInfoWire.","In security tests, assert this exact error is thrown for traversal payloads — its absence is a vulnerability."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"function isSafeRelativePath(value: string): boolean {\n  const segs = value.replaceAll(\"\\\\\", \"/\").split(\"/\");\n  return !segs.includes(\"..\");\n}\n// check before accepting an offer\nif ([...(info.FilesToTransfer ?? []), ...(info.EmptyFoldersToTransfer ?? [])]\n    .some((f) => !isSafeRelativePath(f.fr ?? \".\"))) {\n  rejectOffer(\"traversal attempt\");\n}","typeGuard":null,"tryCatchPattern":"try {\n  const offer = validateSenderInfo(info);\n} catch (error) {\n  if (error instanceof Error && error.message.startsWith(\"Remote path escapes the destination\")) {\n    abortTransferAndWarnUser(\"peer attempted path traversal\");\n    return;\n  }\n  throw error;\n}","preventionTips":["Never relax or catch-and-continue past this guard; escaping paths must abort the transfer.","On the sender, canonicalize folders before offering so '..' segments never reach the wire.","Compose destination paths only via the validated {folder, name} pair, never raw peer strings."],"tags":["security","path-traversal","path-validation","metadata"],"backgroundTag":null,"analyzedSha":"e25f1bdc04f07f094d50b0a1bf67e2563944b57a","analyzedAt":"2026-08-15T12:53:39.096Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}