{"record":{"id":"e8c337af6db17aa3","repo":"Budibase/budibase","slug":"project-package-contains-unsafe-paths","errorCode":null,"errorMessage":"Project package contains unsafe paths.","messagePattern":"Project package contains unsafe paths\\.","errorType":"http","errorClass":"HTTPError","httpStatus":400,"severity":"error","filePath":"packages/server/src/sdk/workspace/projects/backups/imports.ts","lineNumber":187,"sourceCode":"    !path.startsWith(\"\\\\\") &&\n    !/^[A-Za-z]:/.test(path) &&\n    segments.every(segment => segment !== \"..\" && segment !== \".\")\n  )\n}\n\nconst readDirectoryRecursively = async (\n  dirPath: string,\n  rootPath = dirPath,\n  totals = { files: 0, bytes: 0 }\n): Promise<string[]> => {\n  const entries = await fsp.readdir(dirPath, { withFileTypes: true })\n  const files: string[] = []\n\n  for (const entry of entries) {\n    const fullPath = join(dirPath, entry.name)\n    const relPath = relative(rootPath, fullPath)\n    if (!isSafeArchivePath(relPath)) {\n      throw new HTTPError(\"Project package contains unsafe paths.\", 400)\n    }\n    if (relPath.split(/[\\\\/]/).length > MAX_PATH_SEGMENTS) {\n      throw new HTTPError(\n        \"Project package contains paths that are too deep.\",\n        400\n      )\n    }\n    if (entry.isSymbolicLink()) {\n      throw new HTTPError(\"Project package contains unsupported links.\", 400)\n    }\n    if (entry.isDirectory()) {\n      files.push(\n        ...(await readDirectoryRecursively(fullPath, rootPath, totals))\n      )\n    } else {\n      const stats = await fsp.stat(fullPath)\n      totals.files += 1\n      totals.bytes += stats.size","sourceCodeStart":169,"sourceCodeEnd":205,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/sdk/workspace/projects/backups/imports.ts#L169-L205","documentation":"While recursively reading the extracted package, every path is relativized to the archive root and checked with isSafeArchivePath. If a path escapes the root (e.g. .. traversal, absolute paths) the import aborts with this 400 HTTPError. This prevents zip/tar-slip attacks where a malicious archive writes files outside the target directory.","triggerScenarios":"Importing a package whose archive entries contain path traversal (../), absolute paths, or names that relativize outside rootPath — typically a deliberately malicious or improperly generated archive.","commonSituations":"Third-party or tampered .tar.gz packages; archives built on Windows with odd separators/absolute entries; custom scripts that add entries with leading '/' or '..' components.","solutions":["Inspect the archive listing (tar -tf) for entries with .. or absolute paths and remove them","Regenerate the package with Budibase's own export flow","Only import packages from trusted sources","Sanitize entry names to be relative and contained within the archive root"],"exampleFix":"// before\n// archive entry: ../../etc/passwd\n// after\n// archive entry: files/etc-passwd  (relative, inside package root)","handlingStrategy":"validation","validationCode":"import { isSafeArchivePath } from \"@budibase/backend-core\" // same guard the importer uses\nfor (const entry of entries) {\n  if (!isSafeArchivePath(relative(root, entry))) throw new Error(`Unsafe archive entry: ${entry}`)\n}","typeGuard":"function isContainedPath(relPath: string): boolean {\n  return !!relPath && !relPath.startsWith(\"..\") && !isAbsolute(relPath)\n}","tryCatchPattern":"try {\n  await importProject(packagePath)\n} catch (e) {\n  if (e instanceof HTTPError && e.message.includes(\"unsafe paths\")) {\n    // reject/inspect the untrusted package — do not retry as-is\n  } else throw e\n}","preventionTips":["Only import packages from trusted sources","Inspect archives (tar -tf) for ../ or absolute entries before import","Build packages with Budibase export, not custom scripts"],"tags":["security","path-traversal","import","backups"],"backgroundTag":"zip-slip-path-traversal","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}