{"record":{"id":"5bbbc82a419e6e5e","repo":"Yeachan-Heo/oh-my-codex","slug":"refusing-to-back-up-artifact-path-outside-contr","errorCode":null,"errorMessage":"Refusing to back up ${artifact.path} outside controlled backup root.","messagePattern":"Refusing to back up (.+?) outside controlled backup root\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/cli/setup.ts","lineNumber":1974,"sourceCode":"}\n\nasync function ensureSnapshotBackup(\n\tartifact: NativeHookTransactionArtifact,\n\tbackupContext: SetupBackupContext,\n\toptions: Pick<SetupOptions, \"dryRun\" | \"verbose\">,\n\ttracker: RegularFileDurabilityTracker,\n): Promise<boolean> {\n\tconst bytes = artifact.before.bytes;\n\tif (bytes === null) return false;\n\tconst backupPath = nativeHookTransactionBackupPath(artifact.path, backupContext);\n\tif (!options.dryRun) {\n\t\tconst relativeParent = relative(backupContext.baseRoot, dirname(backupPath));\n\t\tif (\n\t\t\tisAbsolute(relativeParent) ||\n\t\t\trelativeParent === \"..\" ||\n\t\t\trelativeParent.startsWith(`..${sep}`)\n\t\t) {\n\t\t\tthrow new Error(`Refusing to back up ${artifact.path} outside controlled backup root.`);\n\t\t}\n\t\tlet currentPath = backupContext.baseRoot;\n\t\tfor (const component of relativeParent.split(sep).filter(Boolean)) {\n\t\t\tcurrentPath = join(currentPath, component);\n\t\t\ttry {\n\t\t\t\tconst currentStat = await lstat(currentPath);\n\t\t\t\tif (currentStat.isSymbolicLink() || !currentStat.isDirectory()) {\n\t\t\t\t\tthrow new Error(`Refusing to use unsafe backup ancestor ${currentPath}.`);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tif (!isMissingPathError(error)) throw error;\n\t\t\t\tawait mkdir(currentPath);\n\t\t\t\tconst createdStat = await lstat(currentPath);\n\t\t\t\tif (createdStat.isSymbolicLink() || !createdStat.isDirectory()) {\n\t\t\t\t\tthrow new Error(`Refusing to use unsafe created backup ancestor ${currentPath}.`);\n\t\t\t\t}\n\t\t\t}\n\t\t}","sourceCodeStart":1956,"sourceCodeEnd":1992,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/cli/setup.ts#L1956-L1992","documentation":"Before writing a transaction backup, setup computes the backup path relative to the controlled backup root and rejects any path that escapes it (absolute relative path, '..', or '..' prefix). This is a path-traversal guard ensuring backups never land outside the managed backup root.","triggerScenarios":"An artifact path containing traversal segments (../) or an absolute path that, when joined under backupContext.baseRoot, resolves outside it.","commonSituations":"Malicious or malformed artifact paths, tampered backup context, or bugs constructing backupPath from user-supplied hook file names.","solutions":["Sanitize artifact.path: strip .., absolute prefixes, and symlinked segments before constructing the backup path","Verify backupContext.baseRoot is the intended controlled root","Log artifact.path and the computed backupPath to spot the escaping segment","Treat this as a security signal: audit where artifact.path originates"],"exampleFix":"// before\nconst backupPath = join(backupContext.baseRoot, artifact.path);\n// after\nconst safeRel = artifact.path.split(/[\\\\/]/).filter((p) => p && p !== \".\" && p !== \"..\").join(sep);\nconst backupPath = join(backupContext.baseRoot, safeRel);","handlingStrategy":"validation","validationCode":"const isSafeRelative = (p: string) =>\n  !p.includes(\"..\") && !isAbsolute(p) && p.split(/[\\\\/]/).every((s) => s && s !== \".\");\nif (!isSafeRelative(artifact.path)) throw new Error(`unsafe artifact path: ${artifact.path}`);","typeGuard":"const isSafeArtifactPath = (p: string): p is `${string}.${string}` =>\n  typeof p === \"string\" && !isAbsolute(p) && !p.split(/[\\\\/]/).includes(\"..\");","tryCatchPattern":"try { await createBackup(artifact); } catch (e) { if (e instanceof Error && e.message.startsWith(\"Refusing to back up\")) { /* sanitize artifact.path and retry */ } else throw e; }","preventionTips":["Never build artifact paths from raw user input","Normalize and reject '..'/absolute segments before joining","Audit path sources as a security hygiene step"],"tags":["path-traversal","security","backup","filesystem"],"backgroundTag":"path-traversal-blocked","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}