{"record":{"id":"b93a8e73f37e85d7","repo":"paperclipai/paperclip","slug":"refusing-public-report-symlink-absolute","errorCode":null,"errorMessage":"Refusing public report symlink ${absolute}","messagePattern":"Refusing public report symlink (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/scripts/publish-runner-protocol-eval-history.mjs","lineNumber":124,"sourceCode":"  if (\n    relativePath.includes(\"\\\\\") ||\n    relativePath.startsWith(\"/\") ||\n    relativePath\n      .split(\"/\")\n      .some((segment) => !segment || segment === \".\" || segment === \"..\")\n  ) {\n    return false;\n  }\n  return SAFE_REPORT_PATHS.some((pattern) => pattern.test(relativePath));\n}\n\nasync function relativeFiles(root, current = root) {\n  const entries = await readdir(current, { withFileTypes: true });\n  const files = [];\n  for (const entry of entries) {\n    const absolute = join(current, entry.name);\n    if (entry.isSymbolicLink())\n      throw new Error(`Refusing public report symlink ${absolute}`);\n    if (entry.isDirectory())\n      files.push(...(await relativeFiles(root, absolute)));\n    else if (entry.isFile())\n      files.push(relative(root, absolute).split(sep).join(\"/\"));\n    else throw new Error(`Refusing unusual public report path ${absolute}`);\n  }\n  return files.sort();\n}\n\nfunction internalHtmlHrefs(content) {\n  return [...content.matchAll(/href\\s*=\\s*[\"']([^\"']+)[\"']/giu)]\n    .map((match) => match[1])\n    .filter((href) => href && !href.startsWith(\"#\"));\n}\n\nexport async function validatePublicProtocolEvalReport(\n  reportRoot,\n  { viewerRoot } = {},","sourceCodeStart":106,"sourceCodeEnd":142,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/scripts/publish-runner-protocol-eval-history.mjs#L106-L142","documentation":"Thrown by relativeFiles in the publish script while walking the report directory: symbolic links are refused outright so the published public report cannot leak or point at files outside the report root. Symlinks (including directory symlinks) abort the publish.","triggerScenarios":"Running the publish against a report directory that contains a symlinked file or subdirectory anywhere in the tree.","commonSituations":"A build step creates convenience symlinks (e.g. 'latest' -> 'v3') inside the report folder; checking out fixtures containing symlinks on macOS/Linux; copying directories with cp -s or rsync --links.","solutions":["Remove symlinks from the report directory or replace them with real copies before publishing.","Build the report with real files only (e.g. cp -L to dereference when copying).","Point the publish script at a clean report directory without symlinked entries."],"exampleFix":"// before\nln -s ./runs/v3 report/latest\n// after\ncp -L ./runs/v3/summary.json report/latest-summary.json","handlingStrategy":"validation","validationCode":"for (const p of walk(reportDir)) {\n  const st = fs.lstatSync(p);\n  if (st.isSymbolicLink()) throw new Error(`symlink in report dir: ${p}`);\n}","typeGuard":null,"tryCatchPattern":"try { await publish(reportDir); } catch (e) { if (e.message.startsWith('Refusing public report symlink')) { console.error('Replace symlinks in the report directory with real files (cp -L).'); process.exit(1); } throw e; }","preventionTips":["Build report artifacts with dereferenced copies (cp -L / rsync without --links).","Scan the report directory for symlinks before publishing.","Avoid creating 'latest' symlink shortcuts inside published folders."],"tags":["filesystem","security","publish"],"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"}