{"record":{"id":"c7b566ab6439bf31","repo":"paperclipai/paperclip","slug":"unsafe-report-navigation-in-file","errorCode":null,"errorMessage":"Unsafe report navigation in ${file}","messagePattern":"Unsafe report navigation in (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/scripts/publish-runner-protocol-eval-history.mjs","lineNumber":205,"sourceCode":"      }\n      if (extname(file) !== \".html\") continue;\n      for (const pattern of ACTIVE_HTML_PATTERNS) {\n        if (pattern.test(content))\n          throw new Error(\n            `Public report contains active or remote HTML: ${file}`,\n          );\n      }\n    }\n    const navigation = payload\n      ? [\n          payload.navigation?.suiteHref,\n          payload.navigation?.previous?.href,\n          payload.navigation?.next?.href,\n        ].filter(Boolean)\n      : [];\n    for (const href of [...internalHtmlHrefs(content), ...navigation]) {\n      if (typeof href !== \"string\" || /[?:\\\\]|^\\/|^[a-z]+:/i.test(href))\n        throw new Error(`Unsafe report navigation in ${file}`);\n      const clean = href.split(\"#\", 1)[0].split(\"?\", 1)[0];\n      const target = resolve(\n        root,\n        ...file.split(\"/\").slice(0, -1),\n        ...clean.split(\"/\"),\n      );\n      const rel = relative(root, target).split(sep).join(\"/\");\n      if (\n        !isPublicProtocolEvalPath(rel) ||\n        !(await lstat(target).catch(() => null))?.isFile()\n      ) {\n        throw new Error(\n          `Public report contains a broken or unsafe link in ${file}: ${href}`,\n        );\n      }\n    }\n  }\n  if (viewer) {","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/scripts/publish-runner-protocol-eval-history.mjs#L187-L223","documentation":"Thrown by validatePublicProtocolEvalReport when any href found in a report file (or a rich-attempt navigation href) fails the safe-navigation regex: it contains ?, \\, starts with /, or uses an absolute scheme like http:. All internal links must be plain relative paths (optionally with #fragment) so the published site cannot navigate off-host or escape the report root via path tricks.","triggerScenarios":"A report page links with href=\"https://...\", href=\"/index.html\", href=\"page.html?x=1\", or a backslash-containing path; a rich attempt's navigation.suiteHref/previous/next values built by the viewer payload contain query strings or absolute paths.","commonSituations":"Template generator emits absolute site-root URLs assuming a server rather than a static bundle; links carry UTM or cache-busting query parameters; Windows-built generator emits backslash separators in hrefs; an agent-authored transcript contains raw markdown links to external sites that get converted to <a href> unmodified.","solutions":["Rewrite the offending link to a relative path without query string, backslashes, or leading slash (e.g. ../index.html instead of /index.html).","Fix the report generator to emit root-relative-free, forward-slash, query-free hrefs.","Strip fragments/queries at generation time and encode external references as plain text rather than anchors.","For rich attempts, fix the navigation payload construction so suiteHref/previous.href/next.href are plain relative paths."],"exampleFix":"// before\n<a href=\"/tests/foo.html?run=7\">Foo</a>\n// after\n<a href=\"../tests/foo.html\">Foo</a>","handlingStrategy":"validation","validationCode":"const isSafeHref = (href) =>\n  typeof href === \"string\" && !/[?:\\\\]|^\\/|^[a-z]+:/i.test(href);\nexport const assertSafeHrefs = (html) => {\n  for (const m of html.matchAll(/href\\s*=\\s*[\"']([^\"']+)[\"']/giu)) {\n    if (m[1] && !m[1].startsWith(\"#\") && !isSafeHref(m[1]))\n      throw new Error(`Unsafe href: ${m[1]}`);\n  }\n};","typeGuard":"const isSafeHref = (href) =>\n  typeof href === \"string\" && !/[?:\\\\]|^\\/|^[a-z]+:/i.test(href);","tryCatchPattern":"try {\n  await validatePublicProtocolEvalReport(reportRoot, { viewerRoot });\n} catch (err) {\n  if (String(err.message).startsWith(\"Unsafe report navigation in\")) {\n    console.error(\"Rewrite the offending link as a plain relative path (no scheme, query, backslash, or leading slash).\");\n  }\n  throw err;\n}","preventionTips":["Generate hrefs with a helper that normalizes to forward-slash relative paths.","Never emit query strings or cache-busting parameters in report links.","Test report generation on both POSIX and Windows so separators stay forward slashes.","Convert external references in transcripts to plain text, not anchors."],"tags":["html","path-safety","publish-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-14T05:17:10.506Z"}