{"record":{"id":"19e3c14f242384bf","repo":"different-ai/openwork","slug":"archive-contains-an-unsafe-path","errorCode":null,"errorMessage":"Archive contains an unsafe path","messagePattern":"Archive contains an unsafe path","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"apps/desktop/electron/workspace-archive.mjs","lineNumber":299,"sourceCode":"      excluded,\n    }, null, 2)}\\n`,\n  });\n  await writeZip(outputPath, files);\n  return { outputPath, included: included.length, excluded };\n}\n\nexport async function importWorkspaceConfig({ archivePath, targetDir, name }) {\n  if (await pathExists(targetDir)) {\n    if (!(await isDirectoryEmpty(targetDir))) throw new Error(\"Target folder must be empty\");\n  }\n  await mkdir(targetDir, { recursive: true });\n\n  const archiveStats = await stat(archivePath);\n  if (archiveStats.size > MAX_ARCHIVE_BYTES) throw new Error(\"Workspace archive is too large.\");\n  const buffer = await readFile(archivePath);\n  for (const entry of listZipEntries(buffer)) {\n    if (entry.name === \"manifest.json\" || entry.name.endsWith(\"/\")) continue;\n    if (!isSafeArchivePath(entry.name)) throw new Error(\"Archive contains an unsafe path\");\n    if (!(entry.name === \"opencode.json\" || entry.name.startsWith(\".opencode/\"))) continue;\n    if (isSecretName(path.basename(entry.name))) continue;\n    const outPath = path.join(targetDir, ...entry.name.split(\"/\"));\n    await mkdir(path.dirname(outPath), { recursive: true });\n    await writeFile(outPath, readZipEntryData(buffer, entry));\n  }\n\n  const opencodeDir = path.join(targetDir, \".opencode\");\n  if (!(await pathExists(opencodeDir))) throw new Error(\"Archive is missing .opencode config\");\n\n  const openworkPath = path.join(opencodeDir, \"openwork.json\");\n  let preset = \"starter\";\n  let workspaceName = typeof name === \"string\" && name.trim() ? name.trim() : null;\n\n  if (await pathExists(openworkPath)) {\n    const raw = await readFile(openworkPath, \"utf8\");\n    try {\n      const config = JSON.parse(raw);","sourceCodeStart":281,"sourceCodeEnd":317,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/apps/desktop/electron/workspace-archive.mjs#L281-L317","documentation":"importWorkspaceConfig imports a workspace config ZIP and extracts only safe entries. Before writing any file it runs isSafeArchivePath, which rejects absolute paths, Windows drive letters, and any segment that normalizes to '..' or an empty part. This is a zip-slip guard: it prevents a crafted archive from writing files outside targetDir via traversal paths.","triggerScenarios":"Calling importWorkspaceConfig({ archivePath, targetDir }) with a ZIP whose entry names contain '..' segments (e.g. '../../.bashrc'), start with '/', or look like 'C:\\evil'.","commonSituations":"Importing an archive edited or rebuilt by a third-party tool that stores backslash or drive-letter paths; a shared/hand-modified archive; a malicious archive from an untrusted source.","solutions":["Open the archive and inspect entry names for '..', leading '/', or 'X:' prefixes; rebuild the archive with plain relative paths (e.g. '.opencode/agents/foo.md').","Re-export the archive with exportWorkspaceConfig from a working workspace, which always emits safe relative names.","If a tool keeps producing backslash names, normalize them to '/' before zipping — ZIP entry names must use forward slashes."],"exampleFix":"// before: entry name '../../evil.sh' aborts the whole import\n// after: repack the archive with safe relative entry names\nzip -r workspace.zip manifest.json opencode.json .opencode/","handlingStrategy":"validation","validationCode":"import { listZipEntries } from \"./workspace-archive.mjs\";\nfunction assertSafeArchive(buffer) {\n  for (const e of listZipEntries(buffer)) {\n    if (e.name === \"manifest.json\" || e.name.endsWith(\"/\")) continue;\n    if (e.name.startsWith(\"/\") || /^[A-Za-z]:/.test(e.name) ||\n        e.name.split(\"/\").some((p) => p === \"..\" || p === \"\")) {\n      throw new Error(`Unsafe archive entry: ${e.name}`);\n    }\n  }\n}","typeGuard":"function isSafeArchivePath(name) {\n  if (!name || name.startsWith(\"/\") || /^[A-Za-z]:/.test(name)) return false;\n  const parts = name.split(\"/\");\n  return !parts.some((p) => p === \"..\" || p === \"\");\n}","tryCatchPattern":"try {\n  await importWorkspaceConfig({ archivePath, targetDir });\n} catch (err) {\n  if (err.message === \"Archive contains an unsafe path\") {\n    // reject the archive; do not retry, it is malicious or corrupt\n  } else throw err;\n}","preventionTips":["Only import archives produced by exportWorkspaceConfig or a trusted source.","Pre-scan ZIP entry names before import and reject any with '..' or absolute segments.","Keep targetDir empty and dedicated so a bad import cannot clobber other files."],"tags":["security","zip-slip","path-traversal","import"],"backgroundTag":"zip-slip-path-traversal","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}