{"record":{"id":"8c9e0c5141e4461d","repo":"nanocoai/nanoclaw","slug":"inbox-safety-inbox-dir-escaped-inbox-root","errorCode":null,"errorMessage":"inbox-safety: inbox dir escaped inbox root","messagePattern":"inbox-safety: inbox dir escaped inbox root","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"src/inbox-safety.ts","lineNumber":75,"sourceCode":"  for (const dir of [inboxRoot, inboxDir]) {\n    try {\n      const st = fs.lstatSync(dir);\n      if (st.isSymbolicLink() || !st.isDirectory()) {\n        log.warn('inbox-safety: rejecting unsafe inbox path', { ...context, dir });\n        return null;\n      }\n    } catch {\n      // Does not exist yet — fine, mkdir below creates it.\n    }\n  }\n\n  fs.mkdirSync(inboxDir, { recursive: true });\n\n  try {\n    const realInboxDir = fs.realpathSync(inboxDir);\n    const realInboxRoot = fs.realpathSync(inboxRoot);\n    if (!isPathInside(realInboxRoot, realInboxDir)) {\n      log.warn('inbox-safety: inbox dir escaped inbox root', { ...context, inboxDir });\n      return null;\n    }\n    return realInboxDir;\n  } catch (err) {\n    log.warn('inbox-safety: failed to resolve inbox dir', { ...context, inboxDir, err });\n    return null;\n  }\n}\n","sourceCodeStart":57,"sourceCodeEnd":84,"githubUrl":"https://github.com/nanocoai/nanoclaw/blob/294ef2aee85218b23ad30eda9dfe10e590b54a8c/src/inbox-safety.ts#L57-L84","documentation":"After mkdir, realpath of the created inbox dir does not sit inside realpath of the inbox root — meaning the resolved location escaped the sandbox. The function returns null and attachment extraction is refused for that message.","triggerScenarios":"ensureContainedInboxDir: realpathSync(inboxDir) resolves outside realpathSync(inboxRoot), e.g. because inboxRoot itself traverses a symlinked parent that realpaths elsewhere while inboxDir was created on a different bind mount.","commonSituations":"Docker bind mounts where host and container resolve the same logical path to different real paths; macOS /var vs /private/var symlink divergence between the two realpath calls; overlapping mounts shadowing the inbox root.","solutions":["Compare realpath of the inbox root from host vs inside container; align mounts so both resolve identically","Avoid symlinked components in the session-data mount path","On macOS, use /private/var-prefixed or Users-prefixed absolute paths consistently","Reproduce with realpath on both sides, then adjust the mount source/target"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import fs from 'node:fs';\nimport path from 'node:path';\nfunction resolvesInside(root: string, child: string): boolean {\n  const r = fs.realpathSync(root);\n  const c = fs.realpathSync(child);\n  return c === r || c.startsWith(r + path.sep);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Use canonical absolute paths (no symlinked components) for session-data mounts","On macOS beware /var vs /private/var divergence between host and container","Keep host and container mount resolution identical; test with realpath on both sides"],"tags":["security","realpath","bind-mount","attachments","inbox"],"backgroundTag":"sandbox-escape-path-rejected","analyzedSha":"294ef2aee85218b23ad30eda9dfe10e590b54a8c","analyzedAt":"2026-08-28T13:59:10.357Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}