{"record":{"id":"e42dfc763a379d7e","repo":"Budibase/budibase","slug":"project-package-contains-unsupported-links","errorCode":null,"errorMessage":"Project package contains unsupported links.","messagePattern":"Project package contains unsupported links\\.","errorType":"http","errorClass":"HTTPError","httpStatus":400,"severity":"error","filePath":"packages/server/src/sdk/workspace/projects/backups/imports.ts","lineNumber":196,"sourceCode":"  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\n      if (totals.files > MAX_PACKAGE_FILES) {\n        throw new HTTPError(\"Project package contains too many files.\", 400)\n      }\n      if (totals.bytes > MAX_EXTRACTED_SIZE_BYTES) {\n        throw new HTTPError(\"Project package is too large.\", 400)\n      }\n      files.push(fullPath)\n    }\n  }","sourceCodeStart":178,"sourceCodeEnd":214,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/sdk/workspace/projects/backups/imports.ts#L178-L214","documentation":"readDirectoryRecursively rejects any entry where entry.isSymbolicLink() is true, throwing this 400 HTTPError. Symbolic links inside an extracted archive could point anywhere on the host filesystem, so they are treated as unsafe and the import aborts.","triggerScenarios":"Importing a package whose extracted directory tree contains symlinks — commonly archives created with tar's default symlink preservation, or packages built on systems that use symlinks for shared assets/node_modules.","commonSituations":"Building packages with `tar -czf` over a tree containing symlinks; Unix build machines linking node_modules or static assets; tampered packages using links to escape the extraction root.","solutions":["Rebuild the package dereferencing symlinks (tar --dereference / cp -L) so all entries are real files","Remove symlinked entries from the archive","Create the package with Budibase's export flow instead of manual archiving","Audit untrusted packages for links before importing"],"exampleFix":"// before\ntar -czf app.tar.gz app/            # preserves symlinks\n// after\ntar -czf --dereference app.tar.gz app/   # stores link targets as regular files","handlingStrategy":"validation","validationCode":"const entries = await fsp.readdir(dir, { withFileTypes: true })\nif (entries.some(e => e.isSymbolicLink())) throw new Error(\"Package contains symlinks; dereference before importing\")","typeGuard":"null","tryCatchPattern":"try {\n  await importProject(packagePath)\n} catch (e) {\n  if (e instanceof HTTPError && e.message.includes(\"unsupported links\")) {\n    // rebuild archive with --dereference and retry\n  } else throw e\n}","preventionTips":["Create archives with tar --dereference or cp -L","Avoid symlinked assets/node_modules in packaged trees","Prefer the official export flow over manual archiving"],"tags":["security","symlink","import","backups"],"backgroundTag":"unsafe-archive-symlink","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}