{"record":{"id":"5416d7db410a0baf","repo":"heygen-com/hyperframes","slug":"unsafe-target-target-resolves-outside-destdi","errorCode":null,"errorMessage":"Unsafe target \"${target}\": resolves outside destDir ${destDir}.","messagePattern":"Unsafe target \"(.+?)\": resolves outside destDir (.+?)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"packages/cli/src/registry/installer.ts","lineNumber":107,"sourceCode":" * Reject target paths that would escape `destDir`. Mirrors the pattern check\n * in `packages/core/schemas/registry-item.json#files.items.target`, but runs at\n * install time so a registry that bypasses schema validation still can't write\n * outside the project.\n */\nexport function assertSafeTarget(destDir: string, target: string): void {\n  if (isAbsolute(target)) {\n    throw new Error(`Unsafe target \"${target}\": absolute paths are not allowed.`);\n  }\n  if (/(^|[/\\\\])\\.\\.([/\\\\]|$)/.test(target)) {\n    throw new Error(`Unsafe target \"${target}\": path segments may not contain \"..\".`);\n  }\n  if (/^[A-Za-z]:[/\\\\]/.test(target)) {\n    throw new Error(`Unsafe target \"${target}\": Windows drive letters are not allowed.`);\n  }\n  const resolved = resolve(destDir, target);\n  const rel = relative(resolve(destDir), resolved);\n  if (rel.startsWith(\"..\") || isAbsolute(rel)) {\n    throw new Error(`Unsafe target \"${target}\": resolves outside destDir ${destDir}.`);\n  }\n}\n\nfunction isInstalledRegistryBlockComposition(item: RegistryItem, file: FileTarget): boolean {\n  return (\n    item.type === \"hyperframes:block\" &&\n    file.type === \"hyperframes:composition\" &&\n    file.target.toLowerCase().endsWith(\".html\")\n  );\n}\n\nfunction addRegistryItemMarker(source: string, item: RegistryItem): string {\n  if (/^\\s*<!--\\s*hyperframes-registry-item:[^>]*-->/i.test(source.slice(0, 512))) {\n    return source;\n  }\n\n  return `<!-- hyperframes-registry-item: ${item.name} -->\\n${source}`;\n}","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/heygen-com/hyperframes/blob/c2996c8626135db5253519359d8a063d3bafad8d/packages/cli/src/registry/installer.ts#L89-L125","documentation":"Thrown by assertSafeTarget as the final backstop when a target passes the absolute/.. /drive-letter checks but still resolves outside destDir after resolve(). The relative() of the resolved path against destDir is computed; if it starts with '..' or is absolute the target escapes and is rejected.","triggerScenarios":"assertSafeTarget computes resolved=resolve(destDir,target) and rel=relative(resolve(destDir),resolved); if rel starts with '..' or isAbsolute(rel) → throw at installer.ts:107. Catches symlink/encoding edge cases the earlier regexes miss.","commonSituations":"A destDir that is itself a symlink so resolve() lands elsewhere; a target with encoded/unicode separators that defeated the '..' regex but resolves outside; an unusual platform path normalization; a target that becomes absolute after resolution.","solutions":["Make the target a simple relative path contained within destDir","Resolve destDir and ensure it isn't a symlink that escapes its declared location","Avoid unicode/encoded characters in target paths","Re-run with a target you can confirm stays under the project root"],"exampleFix":"// before (destDir is a symlink escaping its dir)\n{ \"target\": \"x.html\" }   // resolves outside real destDir\n// after\n// point destDir at the real directory, or use a target under the resolved root","handlingStrategy":"validation","validationCode":"import { resolve, relative, isAbsolute } from \"node:path\";\nfunction targetWithin(destDir: string, target: string): boolean {\n  const rel = relative(resolve(destDir), resolve(destDir, target));\n  return !rel.startsWith(\"..\") && !isAbsolute(rel);\n}","typeGuard":"import { resolve, relative, isAbsolute } from \"node:path\";\nfunction isTargetContained(destDir: string, target: string): boolean {\n  const rel = relative(resolve(destDir), resolve(destDir, target));\n  return !rel.startsWith(\"..\") && !isAbsolute(rel);\n}","tryCatchPattern":null,"preventionTips":["Point destDir at a real (non-symlink-escaping) directory","Use simple relative targets with no encoded/unicode separators","Re-check containment after resolving symlinks in destDir"],"tags":["registry","security","path-traversal","validation","symlink"],"backgroundTag":null,"analyzedSha":"c2996c8626135db5253519359d8a063d3bafad8d","analyzedAt":"2026-08-12T22:18:56.877Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}