{"record":{"id":"6150bb3f7242e15c","repo":"can1357/oh-my-pi","slug":"archive-path-cannot-contain","errorCode":null,"errorMessage":"Archive path cannot contain '..'","messagePattern":"Archive path cannot contain '\\.\\.'","errorType":"validation","errorClass":"ToolError","httpStatus":null,"severity":"critical","filePath":"packages/coding-agent/src/tools/write.ts","lineNumber":473,"sourceCode":"\tif (isEnoent(error)) return true;\n\treturn typeof error === \"object\" && error !== null && \"code\" in error && error.code === \"ENOTDIR\";\n}\n\nfunction normalizeArchiveWriteSubPath(rawPath: string): string {\n\tconst normalized = rawPath.replace(/\\\\/g, \"/\");\n\tif (normalized.length === 0) {\n\t\tthrow new ToolError(\"Archive write path must target a file inside the archive\");\n\t}\n\tif (normalized.endsWith(\"/\")) {\n\t\tthrow new ToolError(\"Archive write path must target a file, not a directory\");\n\t}\n\n\tconst parts = normalized.split(\"/\");\n\tconst normalizedParts: string[] = [];\n\tfor (const part of parts) {\n\t\tif (!part || part === \".\") continue;\n\t\tif (part === \"..\") {\n\t\t\tthrow new ToolError(\"Archive path cannot contain '..'\");\n\t\t}\n\t\tnormalizedParts.push(part);\n\t}\n\n\tif (normalizedParts.length === 0) {\n\t\tthrow new ToolError(\"Archive write path must target a file inside the archive\");\n\t}\n\n\treturn normalizedParts.join(\"/\");\n}\n\nfunction parseSqliteWriteTarget(subPath: string, queryString: string): { table: string; key?: string } {\n\tif (queryString.trim().length > 0) {\n\t\tthrow new ToolError(\"SQLite write paths do not support query parameters\");\n\t}\n\n\tconst normalized = subPath.replace(/^:+/, \"\").trim();\n\tif (!normalized) {","sourceCodeStart":455,"sourceCodeEnd":491,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/write.ts#L455-L491","documentation":"Path traversal is forbidden inside archive targets: any '..' segment in the in-archive sub-path is rejected outright to prevent writing outside the archive entry namespace (zip-slip style attacks).","triggerScenarios":"write({ path: \"bundle.zip/../evil.txt\", content }) or any target whose sub-path contains a '..' segment.","commonSituations":"Relative-path mistakes when constructing archive entry names; attempts to escape the archive root, possibly via untrusted input.","solutions":["Remove '..' segments; archive entries are always relative to the archive root","Sanitize/normalize user-supplied entry names before composing the write target"],"exampleFix":"// before\nwrite({ path: \"bundle.zip/../escape.txt\", content: \"x\" })\n// after\nwrite({ path: \"bundle.zip/escape.txt\", content: \"x\" })","handlingStrategy":"validation","validationCode":"function assertNoTraversal(entry: string): void {\n  const parts = entry.replace(/\\\\/g, \"/\").split(\"/\");\n  if (parts.includes(\"..\")) throw new Error(`archive path traversal rejected: ${entry}`);\n}\nassertNoTraversal(inArchiveSubPath);","typeGuard":null,"tryCatchPattern":"try {\n  await write({ path: `bundle.zip/${entry}`, content });\n} catch (e) {\n  if (e instanceof ToolError && e.message.includes(\"cannot contain '..'\")) {\n    throw new Error(`Refusing unsafe archive entry: ${entry}`);\n  }\n  throw e;\n}","preventionTips":["Sanitize any user-supplied entry names: reject or resolve '..' before composing targets","Treat '..' in archive targets as a security signal, never a legit path","Normalize entry names to root-relative form before write"],"tags":["archive","path-traversal","security"],"backgroundTag":"path-traversal-detected","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}