{"record":{"id":"3b90734388d7f744","repo":"paperclipai/paperclip","slug":"label-escapes-its-declared-root","errorCode":null,"errorMessage":"${label} escapes its declared root","messagePattern":"(.+?) escapes its declared root","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/scripts/runner-protocol-eval-campaign.mjs","lineNumber":52,"sourceCode":"\nasync function loadObject(path) {\n  const value = JSON.parse(await readFile(path, \"utf8\"));\n  if (value === null || Array.isArray(value) || typeof value !== \"object\") {\n    throw new Error(`Expected a JSON object: ${path}`);\n  }\n  return value;\n}\n\nfunction safeId(value, label = \"identifier\") {\n  const result = String(value ?? \"\");\n  if (!SAFE_ID.test(result)) throw new Error(`Unsafe ${label}: ${result}`);\n  return result;\n}\n\nfunction inside(root, candidate, label) {\n  const rel = relative(resolve(root), resolve(candidate));\n  if (!rel || rel === \"..\" || rel.startsWith(`..${sep}`)) {\n    throw new Error(`${label} escapes its declared root`);\n  }\n  return resolve(candidate);\n}\n\nexport function credentialForConfig(config) {\n  if (config.provider === \"opencode\") return \"OPENROUTER_API_KEY\";\n  if (config.provider === \"claude_managed\") return \"ANTHROPIC_API_KEY\";\n  if (config.provider === \"aws_agentcore\") return \"AWS_AGENTCORE_OIDC\";\n  if (config.provider === \"codex\" || config.provider === undefined) {\n    return \"OPENAI_API_KEY\";\n  }\n  if (config.provider === \"acpx\") {\n    if (config.acpxAgent === \"pi\") return \"OPENROUTER_API_KEY\";\n    if (config.acpxAgent === \"claude\") return \"ANTHROPIC_API_KEY\";\n    if (config.acpxAgent === \"codex\") return \"OPENAI_API_KEY\";\n  }\n  throw new Error(\n    `No credential policy for ${config.provider ?? \"codex\"}/${config.acpxAgent ?? \"default\"}`,","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/scripts/runner-protocol-eval-campaign.mjs#L34-L70","documentation":"inside() resolves a candidate path against a declared root and rejects any path that escapes that root: empty relative result (candidate equals root), '..', or any path starting with '../'. It is a path-traversal guard applied to campaign roster and config paths so untrusted JSON cannot reference files outside the eval program directory.","triggerScenarios":"Calling inside(root, candidate, label) where candidate is absolute outside root, uses '..' segments, or equals the root itself — e.g. lane.roster = '../secrets/roster.json' or an absolute path to another directory.","commonSituations":"A roster or config field in campaign JSON referencing a shared file via '../'; moving the campaign file so relative paths now resolve outside the root; typos like '../../rosters/x.json'; malicious or accidentally absolute paths.","solutions":["Move the referenced file inside the declared root directory and update the JSON to a relative path within it.","Remove any '..' segments or absolute paths from roster/config fields in the campaign file.","If a file must be shared, symlink or copy it into the program root instead of referencing it across the boundary."],"exampleFix":"// before\n\"roster\": \"../../shared/roster.json\"\n// after: copy roster.json into evals/paperclip-runner/rosters/ and use\n\"roster\": \"rosters/roster.json\"","handlingStrategy":"validation","validationCode":"const rel = relative(resolve(root), resolve(candidate));\nif (!rel || rel === \"..\" || rel.startsWith(`..${sep}`)) throw new Error(`${label} escapes its declared root`);","typeGuard":"const staysInside = (root, candidate) => {\n  const rel = relative(resolve(root), resolve(candidate));\n  return Boolean(rel) && rel !== \"..\" && !rel.startsWith(`..${require(\"path\").sep}`);\n};","tryCatchPattern":"try {\n  rosterPath = inside(programRoot, lane.roster, \"Campaign roster\");\n} catch (err) {\n  if (err.message.endsWith(\"escapes its declared root\")) console.error(`Use a path relative to and inside the program root for: ${lane.roster}`);\n  throw err;\n}","preventionTips":["Always reference roster/config files with paths relative to the program root.","Never use '..' or absolute paths in campaign JSON path fields.","Re-verify relative paths after moving or renaming campaign files."],"tags":["path-traversal","security","validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}