{"record":{"id":"a53aa9fd064802ef","repo":"santifer/career-ops","slug":"label-escapes-the-tracker-workspace-abspath-workspaceroot","errorCode":null,"errorMessage":"${label} escapes the tracker workspace: ${absPath} (workspaceRoot=${workspace} canonical=${canonical} rel=${rel})","messagePattern":"(.+?) escapes the tracker workspace: (.+?) \\(workspaceRoot=(.+?) canonical=(.+?) rel=(.+?)\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"generate-pdf.mjs","lineNumber":131,"sourceCode":"    // Canonicalization failed (realpath raced away, permission error): containment\n    // is unprovable, so fail closed rather than fall back to a lexical form that a\n    // symlinked ancestor could slip past. Named as its own failure, not as an\n    // escape: an intermittent CI-only hit of this guard (#3162) was undiagnosable\n    // while both branches threw the same message — \"escapes\" points a reader at\n    // the path, when the actual event was realpath failing underneath it.\n    throw new Error(\n      `${label} could not be canonicalized against the tracker workspace`\n      + ` (${/** @type {any} */ (err)?.code || 'realpath failed'} on ${probe}): ${absPath}`,\n    );\n  }\n  const workspace = canonicalWorkspaceRoot();\n  const rel = relative(workspace, canonical);\n  if (rel === '' || rel.startsWith('..') || isAbsolute(rel)) {\n    // #3162: an intermittent macOS-CI-only hit of this branch happens on paths\n    // that are lexically inside the sandbox, and canonicalization SUCCEEDS\n    // before it. Print both sides so the next occurrence names the disagreeing\n    // ancestor outright instead of asking a reader to reconstruct it.\n    throw new Error(\n      `${label} escapes the tracker workspace: ${absPath}`\n      + ` (workspaceRoot=${workspace} canonical=${canonical} rel=${rel})`,\n    );\n  }\n  return absPath;\n}\n\n// Ensure output directory exists (fresh setup)\nmkdirSync(resolve(workspaceRoot, 'output'), { recursive: true });\n\n/**\n * Normalize text for ATS compatibility by converting problematic Unicode.\n *\n * ATS parsers and legacy systems often fail on em-dashes, smart quotes,\n * zero-width characters, and non-breaking spaces. These cause mojibake,\n * parsing errors, or display issues. See issue #1.\n *\n * Only touches body text — preserves CSS, JS, tag attributes, and URLs.","sourceCodeStart":113,"sourceCodeEnd":149,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/generate-pdf.mjs#L113-L149","documentation":"assertInsideWorkspace in generate-pdf.mjs canonicalizes a batch manifest path via realpath and compares it against the canonical workspace root with path.relative. If the relative path is empty, starts with '..', or is absolute, the path resolves outside the workspace and the guard throws with both the raw and canonical paths plus the disagreeing relative segment for diagnosis. This is a fail-closed path-containment (anti path-traversal) check.","triggerScenarios":"A batch manifest entry whose input (after resolve against the manifest dir) realpath-resolves outside the tracker workspace: paths like '../secrets/cv.html', absolute paths to another repo, or a symlink inside the workspace pointing to an external file. Also hit on macOS CI (#3162) when /tmp or the workspace is a symlink so the canonicalized path diverges from the lexical workspace root.","commonSituations":"A generated or tampered manifest referencing paths outside the repo; running with a workspace root that itself sits behind a symlink so canonical workspaceRoot and canonical input disagree on the ancestor; copy-pasted manifest entries from another machine.","solutions":["Fix the manifest entry so input points at a real file inside the tracker workspace (compare the printed workspaceRoot vs canonical in the message)","If the workspace root is behind a symlink (macOS /tmp -> /private/tmp), run from the real (canonical) path or align CAREER_OPS_ROOT with the resolved path","Remove symlinks that point outside the workspace from the input path","Check for a trailing/leading path typo (extra '..', wrong base directory) in the manifest"],"exampleFix":"// before\n{\"input\": \"../../other-repo/cv.html\", \"output\": \"output/cv.pdf\"}\n// after\n{\"input\": \"output/cv-tailored.html\", \"output\": \"output/cv-tailored.pdf\"}","handlingStrategy":"validation","validationCode":"import { resolve, relative, isAbsolute } from 'node:path';\nfunction insideWorkspace(p, root) {\n  const rel = relative(root, resolve(p));\n  return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);\n}\nif (!insideWorkspace(entry.input, workspaceRoot)) throw new Error(`entry escapes workspace: ${entry.input}`);","typeGuard":"const isWorkspacePath = (p) => typeof p === 'string' && insideWorkspace(p, canonicalWorkspaceRoot());","tryCatchPattern":"try {\n  assertInsideWorkspace(entryInput, 'input');\n} catch (err) {\n  if (err.message.includes('escapes the tracker workspace')) {\n    console.error(err.message); // includes workspaceRoot vs canonical to spot symlink divergence\n    process.exit(1);\n  }\n  throw err;\n}","preventionTips":["Keep all manifest input/output paths inside the tracker workspace; use relative paths","On macOS beware /tmp -> /private/tmp symlinks: derive the workspace root from its realpath","Do not use symlinks pointing outside the workspace for inputs","Lint manifests with a containment check before running batch renders"],"tags":["filesystem","path","security","path-traversal"],"backgroundTag":"path-traversal-blocked","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}