{"record":{"id":"6db3c96012ca308b","repo":"ruvnet/ruflo","slug":"flywheel-anchor-path-must-stay-inside-project-root","errorCode":null,"errorMessage":"flywheel anchor path must stay inside project root","messagePattern":"flywheel anchor path must stay inside project root","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/services/harness-project-anchor.ts","lineNumber":64,"sourceCode":"}\n\nexport interface LoadFlywheelAnchorOptions {\n  anchorPath?: string;\n  anchorHash?: string;\n  manifestPath?: string;\n}\n\nfunction normalizeHash(value: string): string {\n  const trimmed = value.trim().toLowerCase();\n  return trimmed.startsWith('sha256:') ? trimmed : `sha256:${trimmed}`;\n}\n\nfunction containedPath(projectRoot: string, requested: string): string {\n  const root = realpathSync(resolve(projectRoot));\n  const absolute = isAbsolute(requested) ? resolve(requested) : resolve(root, requested);\n  const lexical = relative(root, absolute);\n  if (lexical === '..' || lexical.startsWith(`..${sep}`) || isAbsolute(lexical)) {\n    throw new Error('flywheel anchor path must stay inside project root');\n  }\n  const actual = realpathSync(absolute);\n  const physical = relative(root, actual);\n  if (physical === '..' || physical.startsWith(`..${sep}`) || isAbsolute(physical)) {\n    throw new Error('flywheel anchor symlink escapes project root');\n  }\n  return actual;\n}\n\nfunction parseTasks(path: string): { version: string; tasks: HumanEvalTask[] } {\n  const parsed = JSON.parse(readFileSync(path, 'utf8')) as {\n    schemaVersion?: string;\n    version?: string;\n    tasks?: HumanEvalTask[];\n  };\n  if (parsed.schemaVersion && parsed.schemaVersion !== PROJECT_ANCHOR_SCHEMA) {\n    throw new Error(`unsupported flywheel anchor schema: ${parsed.schemaVersion}`);\n  }","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/v3/@claude-flow/cli/src/services/harness-project-anchor.ts#L46-L82","documentation":"Thrown by containedPath() in harness-project-anchor.ts during the LEXICAL path-containment check (before resolving symlinks). The requested anchor path, when resolved relative to the project root, produces a relative path starting with '..' or an absolute path — i.e. it escapes the project root purely by its string form. This is the first layer of path-traversal defense for project-local flywheel anchors.","triggerScenarios":"Calling loadEffectiveFlywheelAnchor(root, { anchorPath: '../sibling/anchor.json' }) or { anchorPath: '/etc/anchor.json' } or any path that resolves outside the project root. Also via a manifest whose `path` field points outside.","commonSituations":"A manifest with an absolute path to a shared anchor outside the repo; a relative '../' path intended to reference a monorepo sibling; user-supplied anchor path that wasn't validated; a path-injection attempt.","solutions":["Use an anchor path that resolves inside the project root (relative paths like ./eval/anchor.json).","If you need a shared anchor, copy or symlink-within-root it into the repo first (the symlink check is separate).","Validate the path with path.relative(root, resolved) and reject if it starts with '..'.","For manifest `path` fields, use paths relative to the manifest location that stay inside root."],"exampleFix":"// before\nloadEffectiveFlywheelAnchor(root, { anchorPath: '../shared/anchor.json', anchorHash });\n// after\ncp ../shared/anchor.json ./eval/anchor.json\nloadEffectiveFlywheelAnchor(root, { anchorPath: './eval/anchor.json', anchorHash });","handlingStrategy":"validation","validationCode":"import { resolve, relative, isAbsolute, sep } from 'node:path';\nfunction assertPathInsideRoot(root: string, requested: string): void {\n  const absolute = isAbsolute(requested) ? resolve(requested) : resolve(root, requested);\n  const rel = relative(root, absolute);\n  if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {\n    throw new Error(`anchor path escapes project root: ${requested}`);\n  }\n}\nassertPathInsideRoot(root, options.anchorPath);","typeGuard":"const isPathInsideRoot = (root: string, requested: string): boolean => {\n  const rel = relative(root, isAbsolute(requested) ? resolve(requested) : resolve(root, requested));\n  return rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);\n};","tryCatchPattern":"try {\n  loadEffectiveFlywheelAnchor(root, { anchorPath, anchorHash });\n} catch (e) {\n  if (e instanceof Error && /must stay inside project root/.test(e.message)) {\n    throw new Error(`anchor path rejected (traversal): ${anchorPath}`);\n  }\n  throw e;\n}","preventionTips":["Use anchor paths relative to the project root (e.g. ./eval/anchor.json).","Never accept absolute or '../' anchor paths from untrusted input.","Validate manifest `path` fields for containment before loading."],"tags":["security","path-traversal","filesystem","validation","anchor"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}