{"record":{"id":"15623821beb01c8d","repo":"Budibase/budibase","slug":"invalid-zip","errorCode":null,"errorMessage":"Invalid zip","messagePattern":"Invalid zip","errorType":"http","errorClass":"BadRequestError","httpStatus":400,"severity":"error","filePath":"packages/server/src/api/controllers/static/index.ts","lineNumber":106,"sourceCode":"const ZIP_SYMLINK_FILE_TYPE = 0o120000\n\nconst validatePWAZipEntries = () => {\n  let fileCount = 0\n  let totalUncompressedSize = 0\n\n  return (entry: {\n    fileName: string\n    uncompressedSize: number\n    externalFileAttributes: number\n  }) => {\n    // extract-zip skips these itself, so don't count them against the limits.\n    if (entry.fileName.startsWith(\"__MACOSX/\")) {\n      return\n    }\n\n    const fileType = (entry.externalFileAttributes >>> 16) & ZIP_FILE_TYPE_MASK\n    if (fileType === ZIP_SYMLINK_FILE_TYPE) {\n      throw new BadRequestError(`Invalid zip`)\n    }\n\n    const depth =\n      entry.fileName.split(\"/\").filter(Boolean).length -\n      (entry.fileName.endsWith(\"/\") ? 0 : 1)\n    if (depth > MAX_PWA_ZIP_DEPTH) {\n      throw new BadRequestError(\n        `Invalid zip - directory depth exceeds ${MAX_PWA_ZIP_DEPTH}`\n      )\n    }\n\n    // Directory entries carry no content, only enforce the depth limit on them.\n    if (entry.fileName.endsWith(\"/\")) {\n      return\n    }\n\n    fileCount++\n    if (fileCount > MAX_PWA_ZIP_FILE_COUNT) {","sourceCodeStart":88,"sourceCodeEnd":124,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/api/controllers/static/index.ts#L88-L124","documentation":"When importing a PWA zip, each entry is scanned for security hazards. The entry's external file attributes are inspected for the unix file-type bits; if the entry is a symbolic link (ZIP_SYMLINK_FILE_TYPE) the zip is rejected as a BadRequestError to prevent symlink-based path traversal / file-overwrite attacks during extraction.","triggerScenarios":"Uploading a PWA zip that contains symlink entries — commonly zips built on macOS or Linux with tools that preserve symlinks (tar->zip conversions, node_modules zips, zips containing __MACOSX metadata); validatePWAZipEntries throws at packages/server/src/api/controllers/static/index.ts:106.","commonSituations":"Building a PWA bundle on macOS where the archiver follows/preserves symlinks; packaging a build output directory that includes symlinked assets; zipping with `zip -y` or equivalent that stores symlinks rather than their targets.","solutions":["Rebuild the zip resolving symlinks to real files (e.g. `zip -r pwa.zip . -y` without -y, or follow links during packaging).","Exclude symlink entries when packaging: filter them out of the archive or delete them from the build output.","Use a bundler/archiver configuration that dereferences symlinks (e.g. archiver with `follow: true`).","Remove macOS junk like __MACOSX/ and .DS_Store and re-export the archive cleanly."],"exampleFix":"// before (preserves symlinks)\nexecSync(\"zip -ry pwa.zip dist/\")\n// after (dereferences symlinks)\nexecSync(\"zip -r pwa.zip dist/\")","handlingStrategy":"validation","validationCode":"// client-side pre-check: reject zips that contain symlinks before upload\n// e.g. with yauzl on the producing side, or shell: `unzip -Z1 pwa.zip` combined with\n// `zipinfo pwa.zip | grep '^l'` — fail fast if any symlink entries exist\nif (execSync(\"zipinfo -1 pwa.zip | awk -F' -> ' 'NR && $2').toString().trim()) {\n  throw new Error(\"Zip contains symlinks; rebuild without them\")\n}","typeGuard":"function isRegularFileEntry(entry: { externalFileAttributes: number }): boolean {\n  const fileType = (entry.externalFileAttributes >>> 16) & 0o170000\n  return fileType !== 0o120000 // S_IFLNK\n}","tryCatchPattern":"try {\n  await uploadPwaZip(zipPath)\n} catch (err) {\n  if (err instanceof Error && err.message === \"Invalid zip\") {\n    console.error(\"Zip rejected: contains symlink entries — rebuild with dereferenced symlinks\")\n  }\n  throw err\n}","preventionTips":["Package build output with symlinks dereferenced (no `zip -y`).","Strip __MACOSX/, .DS_Store and symlinked assets from archives.","Scan archives with zipinfo for 'l' (symlink) entries in CI before upload.","Keep packaging scripts in the repo so the same flags are used everywhere."],"tags":["file-upload","security","zip","pwa"],"backgroundTag":"unsafe-zip-symlink","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}