VYPR
High severity8.7NVD Advisory· Published Apr 3, 2026· Updated Apr 8, 2026

CVE-2026-35214

CVE-2026-35214

Description

Budibase is an open-source low-code platform. Prior to version 3.33.4, the plugin file upload endpoint (POST /api/plugin/upload) passes the user-supplied filename directly to createTempFolder() without sanitizing path traversal sequences. An attacker with Global Builder privileges can craft a multipart upload with a filename containing ../ to delete arbitrary directories via rmSync and write arbitrary files via tarball extraction to any filesystem path the Node.js process can access. This issue has been patched in version 3.33.4.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@budibase/servernpm
< 3.33.43.33.4

Affected products

1

Patches

1
6344d06d7036

Merge pull request #18240 from Budibase/fix/file-write-tar-path

https://github.com/Budibase/budibasemelohaganMar 13, 2026via ghsa
15 files changed · +310 62
  • packages/server/src/api/controllers/plugin/file.ts+8 2 modified
    @@ -1,5 +1,6 @@
     import {
       createTempFolder,
    +  deleteFolderFileSystem,
       getPluginMetadata,
       extractTarball,
     } from "../../../utilities/fileSystem"
    @@ -13,7 +14,12 @@ export async function fileUpload(file: KoaFile) {
         throw new Error("Plugin must be compressed into a gzipped tarball.")
       }
       const path = createTempFolder(file.name.split(".tar.gz")[0])
    -  await extractTarball(file.path, path)
     
    -  return await getPluginMetadata(path)
    +  try {
    +    await extractTarball(file.path, path)
    +    return await getPluginMetadata(path)
    +  } catch (err) {
    +    deleteFolderFileSystem(path)
    +    throw err
    +  }
     }
    
  • packages/server/src/api/controllers/plugin/github.ts+9 2 modified
    @@ -1,4 +1,7 @@
    -import { getPluginMetadata } from "../../../utilities/fileSystem"
    +import {
    +  deleteFolderFileSystem,
    +  getPluginMetadata,
    +} from "../../../utilities/fileSystem"
     import fetch from "node-fetch"
     import { downloadUnzipTarball } from "./utils"
     
    @@ -17,6 +20,7 @@ export async function request(
     
     export async function githubUpload(url: string, name = "", token = "") {
       let githubUrl = url
    +  let path: string | undefined
     
       if (!githubUrl.includes("https://github.com/")) {
         throw new Error("The plugin origin must be from Github")
    @@ -61,13 +65,16 @@ export async function githubUpload(url: string, name = "", token = "") {
       }
     
       try {
    -    const path = await downloadUnzipTarball(
    +    path = await downloadUnzipTarball(
           pluginLastReleaseTarballUrl,
           pluginName,
           headers
         )
         return await getPluginMetadata(path)
       } catch (err: any) {
    +    if (path) {
    +      deleteFolderFileSystem(path)
    +    }
         let errMsg = err?.message || err
         if (errMsg === "unexpected response Not Found") {
           errMsg = "Github release tarball not found"
    
  • packages/server/src/api/controllers/plugin/index.ts+26 8 modified
    @@ -24,6 +24,7 @@ import { npmUpload, urlUpload, githubUpload } from "./uploaders"
     import env from "../../../environment"
     import { clientAppSocket } from "../../../websockets"
     import sdk from "../../../sdk"
    +import { deleteFolderFileSystem } from "../../../utilities/fileSystem"
     
     export async function upload(
       ctx: UserCtx<UploadPluginRequest, UploadPluginResponse>
    @@ -59,35 +60,48 @@ export async function create(
       ctx: UserCtx<CreatePluginRequest, CreatePluginResponse>
     ) {
       const { source, url, headers, githubToken } = ctx.request.body
    +  let directory: string | undefined
    +  let cleanupDirectory: string | undefined
     
       try {
         let metadata: PluginMetadata
    -    let directory: string
     
         // Generating random name as a backup and needed for url
         const name = "PLUGIN_" + Math.floor(100000 + Math.random() * 900000)
     
         switch (source) {
           case PluginSource.NPM: {
    -        const { metadata: metadataNpm, directory: directoryNpm } =
    -          await npmUpload(url, name)
    +        const {
    +          metadata: metadataNpm,
    +          directory: directoryNpm,
    +          cleanupDirectory: cleanupDirectoryNpm,
    +        } = await npmUpload(url, name)
             metadata = metadataNpm
             directory = directoryNpm
    +        cleanupDirectory = cleanupDirectoryNpm
             break
           }
           case PluginSource.GITHUB: {
    -        const { metadata: metadataGithub, directory: directoryGithub } =
    -          await githubUpload(url, name, githubToken)
    +        const {
    +          metadata: metadataGithub,
    +          directory: directoryGithub,
    +          cleanupDirectory: cleanupDirectoryGithub,
    +        } = await githubUpload(url, name, githubToken)
             metadata = metadataGithub
             directory = directoryGithub
    +        cleanupDirectory = cleanupDirectoryGithub
             break
           }
           case PluginSource.URL: {
             const headersObj = headers || {}
    -        const { metadata: metadataUrl, directory: directoryUrl } =
    -          await urlUpload(url, name, headersObj)
    +        const {
    +          metadata: metadataUrl,
    +          directory: directoryUrl,
    +          cleanupDirectory: cleanupDirectoryUrl,
    +        } = await urlUpload(url, name, headersObj)
             metadata = metadataUrl
             directory = directoryUrl
    +        cleanupDirectory = cleanupDirectoryUrl
             break
           }
           default:
    @@ -120,7 +134,7 @@ export async function create(
     
         const doc = await pro.plugins.storePlugin(
           metadata,
    -      directory,
    +      directory!,
           source,
           origin
         )
    @@ -130,6 +144,10 @@ export async function create(
       } catch (err: any) {
         const errMsg = err?.message ? err?.message : err
         ctx.throw(400, `Failed to import plugin: ${errMsg}`)
    +  } finally {
    +    if (cleanupDirectory) {
    +      deleteFolderFileSystem(cleanupDirectory)
    +    }
       }
     }
     
    
  • packages/server/src/api/controllers/plugin/npm.ts+10 3 modified
    @@ -1,4 +1,7 @@
    -import { getPluginMetadata } from "../../../utilities/fileSystem"
    +import {
    +  deleteFolderFileSystem,
    +  getPluginMetadata,
    +} from "../../../utilities/fileSystem"
     import fetch from "node-fetch"
     import { join } from "path"
     import { downloadUnzipTarball } from "./utils"
    @@ -36,6 +39,10 @@ export async function npmUpload(url: string, name: string, headers = {}) {
     
       const path = await downloadUnzipTarball(npmTarballUrl, pluginName, headers)
       const pluginRoot = join(path, "package", "dist")
    -
    -  return await getPluginMetadata(pluginRoot)
    +  try {
    +    return await getPluginMetadata(pluginRoot, path)
    +  } catch (err) {
    +    deleteFolderFileSystem(path)
    +    throw err
    +  }
     }
    
  • packages/server/src/api/controllers/plugin/url.ts+10 3 modified
    @@ -1,12 +1,19 @@
     import { downloadUnzipTarball } from "./utils"
    -import { getPluginMetadata } from "../../../utilities/fileSystem"
    +import {
    +  deleteFolderFileSystem,
    +  getPluginMetadata,
    +} from "../../../utilities/fileSystem"
     
     export async function urlUpload(url: string, name = "", headers = {}) {
       if (!url.includes(".tar.gz")) {
         throw new Error("Plugin must be compressed into a gzipped tarball.")
       }
     
       const path = await downloadUnzipTarball(url, name, headers)
    -
    -  return await getPluginMetadata(path)
    +  try {
    +    return await getPluginMetadata(path)
    +  } catch (err) {
    +    deleteFolderFileSystem(path)
    +    throw err
    +  }
     }
    
  • packages/server/src/api/controllers/plugin/utils.ts+8 4 modified
    @@ -1,17 +1,21 @@
    -import { createTempFolder } from "../../../utilities/fileSystem"
    +import {
    +  createTempFolder,
    +  deleteFolderFileSystem,
    +} from "../../../utilities/fileSystem"
     import { objectStore } from "@budibase/backend-core"
     
     export async function downloadUnzipTarball(
       url: string,
       name: string,
       headers = {}
     ) {
    +  const path = createTempFolder(name)
    +
       try {
    -    const path = createTempFolder(name)
         await objectStore.downloadTarballDirect(url, path, headers)
    -
         return path
       } catch (e: any) {
    -    throw new Error(e.message)
    +    deleteFolderFileSystem(path)
    +    throw e
       }
     }
    
  • packages/server/src/api/routes/tests/plugin.spec.ts+85 3 modified
    @@ -29,9 +29,15 @@ import { Plugin, PluginSource, PluginType } from "@budibase/types"
     import nock from "nock"
     import * as setup from "./utilities"
     import * as github from "../../controllers/plugin/github"
    +import { budibaseTempDir } from "../../../utilities/budibaseDir"
     
     const mockUploadDirectory = objectStore.uploadDirectory as jest.Mock
     const mockDeleteFolder = objectStore.deleteFolder as jest.Mock
    +const getTempDirsWithPrefix = (prefix: string) =>
    +  fs
    +    .readdirSync(budibaseTempDir())
    +    .filter(dir => dir.startsWith(prefix))
    +    .sort()
     
     describe("/plugins", () => {
       let request = setup.getRequest()
    @@ -50,16 +56,19 @@ describe("/plugins", () => {
     
       const createPlugin = async (
         pluginType: "default" | "encoded" = "default",
    -    status?: number
    +    status?: number,
    +    uploadFilename?: string
       ) => {
    -    const filename =
    +    const fixtureFilename =
           pluginType === "encoded"
             ? "_.-encoded-plugin-1.0.0.tar.gz"
             : "comment-box-1.0.2.tar.gz"
     
         return request
           .post(`/api/plugin/upload`)
    -      .attach("file", `src/api/routes/tests/data/${filename}`)
    +      .attach("file", `src/api/routes/tests/data/${fixtureFilename}`, {
    +        filename: uploadFilename || undefined,
    +      })
           .set(config.defaultHeaders())
           .expect("Content-Type", /json/)
           .expect(status ? status : 200)
    @@ -90,6 +99,43 @@ describe("/plugins", () => {
           expect(res.body.message).toEqual("Failed to import plugin: Error")
           expect(events.plugin.imported).toHaveBeenCalledTimes(0)
         })
    +
    +    it("should ignore traversal sequences in uploaded filenames", async () => {
    +      const res = await createPlugin(
    +        "default",
    +        undefined,
    +        "../../../etc/cron.d/plugin.tar.gz"
    +      )
    +
    +      expect(res.body.plugins[0]._id).toEqual("plg_comment-box")
    +      expect(events.plugin.imported).toHaveBeenCalledTimes(1)
    +    })
    +
    +    it("should clean up temp directories when upload extraction fails", async () => {
    +      const invalidTarballPath = path.join(
    +        os.tmpdir(),
    +        `invalid-plugin-${Date.now()}.tar.gz`
    +      )
    +      const uploadId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
    +      const uploadFilename = `broken-plugin-${uploadId}.tar.gz`
    +      const tempDirPrefix = `${uploadFilename.replace(".tar.gz", "")}-`
    +      fs.writeFileSync(invalidTarballPath, "not a tarball")
    +
    +      try {
    +        await request
    +          .post("/api/plugin/upload")
    +          .attach("file", invalidTarballPath, {
    +            filename: uploadFilename,
    +          })
    +          .set(config.defaultHeaders())
    +          .expect("Content-Type", /json/)
    +          .expect(400)
    +
    +        expect(getTempDirsWithPrefix(tempDirPrefix)).toEqual([])
    +      } finally {
    +        fs.unlinkSync(invalidTarballPath)
    +      }
    +    })
       })
     
       describe("fetch", () => {
    @@ -238,6 +284,41 @@ describe("/plugins", () => {
           expect(plugin._id).toEqual("plg_budibase-component")
           expect(events.plugin.imported).toHaveBeenCalled()
         })
    +
    +    it("should clean up temp directories after a successful npm import", async () => {
    +      const packageName = `budibase-component-cleanup-${Date.now()}-${Math.random()
    +        .toString(36)
    +        .slice(2, 10)}`
    +      const tempDirPrefix = `${packageName}-`
    +
    +      nock("https://registry.npmjs.org")
    +        .get(`/${packageName}`)
    +        .reply(200, {
    +          name: packageName,
    +          "dist-tags": {
    +            latest: "1.0.0",
    +          },
    +          versions: {
    +            "1.0.0": {
    +              dist: {
    +                tarball: `https://registry.npmjs.org/${packageName}/-/${packageName}-1.0.1.tgz`,
    +              },
    +            },
    +          },
    +        })
    +        .get(`/${packageName}/-/${packageName}-1.0.1.tgz`)
    +        .replyWithFile(
    +          200,
    +          "src/api/routes/tests/data/budibase-component-1.0.1.tgz"
    +        )
    +
    +      await config.api.plugin.create({
    +        source: PluginSource.NPM,
    +        url: `https://www.npmjs.com/package/${packageName}`,
    +      })
    +
    +      expect(getTempDirsWithPrefix(tempDirPrefix)).toEqual([])
    +    })
       })
     
       describe("url", () => {
    @@ -281,6 +362,7 @@ describe("/plugins", () => {
                 },
               },
               directory: dir,
    +          cleanupDirectory: dir,
             }
           })
     
    
  • packages/server/src/sdk/plugins/index.ts+23 16 modified
    @@ -11,6 +11,7 @@ import { clientAppSocket } from "../../websockets"
     import { sdk as pro } from "@budibase/pro"
     import mappingFile from "./mapping.json"
     import sdk from "../../sdk"
    +import { deleteFolderFileSystem } from "../../utilities/fileSystem"
     
     export async function fetch(type?: PluginType): Promise<Plugin[]> {
       const db = tenancy.getGlobalDB()
    @@ -124,25 +125,31 @@ export async function backfillPluginOrigins() {
     }
     
     export async function processUploaded(plugin: KoaFile, source: PluginSource) {
    -  const { metadata, directory } = await fileUpload(plugin)
    -  pluginCore.validate(metadata.schema)
    +  const { metadata, directory, cleanupDirectory } = await fileUpload(plugin)
    +  try {
    +    pluginCore.validate(metadata.schema)
     
    -  // Only allow components in cloud
    -  if (!env.SELF_HOSTED && metadata.schema?.type !== PluginType.COMPONENT) {
    -    throw new Error("Only component plugins are supported outside of self-host")
    -  }
    +    // Only allow components in cloud
    +    if (!env.SELF_HOSTED && metadata.schema?.type !== PluginType.COMPONENT) {
    +      throw new Error(
    +        "Only component plugins are supported outside of self-host"
    +      )
    +    }
     
    -  // Only allow Svelte 5 plugins on this branch
    -  if (
    -    metadata.schema?.metadata?.svelteMajor !== 5 &&
    -    metadata.schema?.type === PluginType.COMPONENT
    -  ) {
    -    throw new Error("Only Svelte 5 plugins are supported on this branch")
    -  }
    +    // Only allow Svelte 5 plugins on this branch
    +    if (
    +      metadata.schema?.metadata?.svelteMajor !== 5 &&
    +      metadata.schema?.type === PluginType.COMPONENT
    +    ) {
    +      throw new Error("Only Svelte 5 plugins are supported on this branch")
    +    }
     
    -  const doc = await pro.plugins.storePlugin(metadata, directory, source)
    -  clientAppSocket?.emit("plugin-update", { name: doc.name, hash: doc.hash })
    -  return doc
    +    const doc = await pro.plugins.storePlugin(metadata, directory, source)
    +    clientAppSocket?.emit("plugin-update", { name: doc.name, hash: doc.hash })
    +    return doc
    +  } finally {
    +    deleteFolderFileSystem(cleanupDirectory)
    +  }
     }
     
     export const enrichUsedPluginSvelteMajors = async (
    
  • packages/server/src/sdk/plugins/update.ts+4 2 modified
    @@ -194,7 +194,7 @@ async function collectCandidates(
           const download = await githubUpload(plugin.origin.url, plugin.name, token)
     
           const svelteMajor = extractSvelteMajor(download.metadata)
    -      safeRemoveDirectory(download.directory)
    +      safeRemoveDirectory(download.cleanupDirectory)
     
           const originBase = {
             ...plugin.origin,
    @@ -292,6 +292,7 @@ export async function applyPluginUpdates(
         }
     
         let directory: string | undefined
    +    let cleanupDirectory: string | undefined
         try {
           const release = await fetchLatestRelease(plugin.origin.repo, token)
     
    @@ -301,6 +302,7 @@ export async function applyPluginUpdates(
     
           const download = await githubUpload(plugin.origin.url, plugin.name, token)
           directory = download.directory
    +      cleanupDirectory = download.cleanupDirectory
     
           const svelteMajor = extractSvelteMajor(download.metadata)
           if (typeof svelteMajor !== "number" || svelteMajor < 5) {
    @@ -334,7 +336,7 @@ export async function applyPluginUpdates(
             error: err?.message || String(err),
           })
         } finally {
    -      safeRemoveDirectory(directory)
    +      safeRemoveDirectory(cleanupDirectory)
         }
       }
     
    
  • packages/server/src/utilities/fileSystem/app.ts+2 4 modified
    @@ -4,8 +4,8 @@ import { join } from "path"
     import { ObjectStoreBuckets } from "../../constants"
     import environment from "../../environment"
     import { budibaseTempDir } from "../budibaseDir"
    +import { deleteFolderFileSystem, TOP_LEVEL_PATH } from "./filesystem"
     import { shouldServeLocally, updateClientLibrary } from "./clientLibrary"
    -import { TOP_LEVEL_PATH } from "./filesystem"
     
     export const NODE_MODULES_PATH = join(TOP_LEVEL_PATH, "node_modules")
     
    @@ -86,8 +86,6 @@ export const getComponentLibraryManifest = async (library: string) => {
     export const cleanup = (appIds: string[]) => {
       for (let appId of appIds) {
         const path = join(budibaseTempDir(), appId)
    -    if (fs.existsSync(path)) {
    -      fs.rmdirSync(path, { recursive: true })
    -    }
    +    deleteFolderFileSystem(path)
       }
     }
    
  • packages/server/src/utilities/fileSystem/filesystem.ts+34 13 modified
    @@ -1,6 +1,6 @@
     import fs, { PathLike } from "fs"
     import { budibaseTempDir } from "../budibaseDir"
    -import { join } from "path"
    +import { basename, isAbsolute, join, relative, resolve, sep } from "path"
     import env from "../../environment"
     import * as tar from "tar"
     
    @@ -75,25 +75,44 @@ export const streamFile = (path: string) => {
       return fs.createReadStream(path)
     }
     
    +const sanitizeTempFolderPrefix = (item: string) => {
    +  const prefix = basename(item).replace(/[^a-zA-Z0-9._-]/g, "-")
    +  return prefix.length ? prefix : "tmp"
    +}
    +
    +const assertWithinTempDir = (path: string) => {
    +  const tempDir = resolve(budibaseTempDir())
    +  const resolvedPath = resolve(path)
    +  const relativePath = relative(tempDir, resolvedPath)
    +
    +  if (
    +    relativePath === ".." ||
    +    relativePath.startsWith(`..${sep}`) ||
    +    isAbsolute(relativePath)
    +  ) {
    +    throw new Error("Path must be within the Budibase temp directory.")
    +  }
    +
    +  return resolvedPath
    +}
    +
     export const createTempFolder = (item: string) => {
    -  const path = join(budibaseTempDir(), item)
    +  const path = fs.mkdtempSync(
    +    join(budibaseTempDir(), `${sanitizeTempFolderPrefix(item)}-`)
    +  )
       try {
    -    // remove old tmp directories automatically - don't combine
    -    if (fs.existsSync(path)) {
    -      fs.rmSync(path, { recursive: true, force: true })
    -    }
    -    fs.mkdirSync(path)
    +    return assertWithinTempDir(path)
       } catch (err: any) {
    +    fs.rmSync(path, { recursive: true, force: true })
         throw new Error(`Path cannot be created: ${err.message}`)
       }
    -
    -  return path
     }
     
     export const extractTarball = async (fromFilePath: string, toPath: string) => {
    +  const safePath = assertWithinTempDir(toPath)
       await tar.extract({
         file: fromFilePath,
    -    C: toPath,
    +    C: safePath,
       })
     }
     
    @@ -125,10 +144,12 @@ export const findFileRec = (
     /**
      * Remove a folder which is not empty from the file system
      */
    -export const deleteFolderFileSystem = (path: PathLike) => {
    -  if (!fs.existsSync(path)) {
    +export const deleteFolderFileSystem = (path: string) => {
    +  const safePath = assertWithinTempDir(path)
    +
    +  if (!fs.existsSync(safePath)) {
         return
       }
     
    -  fs.rmSync(path, { recursive: true, force: true })
    +  fs.rmSync(safePath, { recursive: true, force: true })
     }
    
  • packages/server/src/utilities/fileSystem/plugin.ts+7 2 modified
    @@ -9,7 +9,8 @@ const DATASOURCE_PATH = join(budibaseTempDir(), "datasource")
     const AUTOMATION_PATH = join(budibaseTempDir(), "automation")
     
     export const getPluginMetadata = async (
    -  path: string
    +  path: string,
    +  cleanupDirectory = path
     ): Promise<PluginUpload> => {
       let pkg: Package
       let schema: PluginSchema
    @@ -32,7 +33,11 @@ export const getPluginMetadata = async (
         )
       }
     
    -  return { metadata: { package: pkg, schema }, directory: path }
    +  return {
    +    metadata: { package: pkg, schema },
    +    directory: path,
    +    cleanupDirectory,
    +  }
     }
     
     async function getPluginImpl(path: string, plugin: Plugin) {
    
  • packages/server/src/utilities/fileSystem/tests/app.spec.ts+41 0 added
    @@ -0,0 +1,41 @@
    +import fs from "fs"
    +import path from "path"
    +import { budibaseTempDir } from "../../budibaseDir"
    +import { cleanup } from "../app"
    +
    +describe("app filesystem cleanup", () => {
    +  it("removes temp folders for the provided app IDs only", () => {
    +    const appId = `app_${Date.now()}`
    +    const appPath = path.join(budibaseTempDir(), appId)
    +    const otherPath = path.join(
    +      budibaseTempDir(),
    +      `other-app-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
    +    )
    +
    +    fs.mkdirSync(appPath, { recursive: true })
    +    fs.mkdirSync(otherPath, { recursive: true })
    +
    +    cleanup([appId])
    +
    +    expect(fs.existsSync(appPath)).toBe(false)
    +    expect(fs.existsSync(otherPath)).toBe(true)
    +
    +    fs.rmSync(otherPath, { recursive: true, force: true })
    +  })
    +
    +  it("rejects cleanup paths that escape the Budibase temp directory", () => {
    +    const outsidePath = path.resolve(
    +      budibaseTempDir(),
    +      "..",
    +      `outside-${Date.now()}`
    +    )
    +    fs.mkdirSync(outsidePath, { recursive: true })
    +
    +    expect(() =>
    +      cleanup([`..${path.sep}${path.basename(outsidePath)}`])
    +    ).toThrow("Path must be within the Budibase temp directory.")
    +    expect(fs.existsSync(outsidePath)).toBe(true)
    +
    +    fs.rmSync(outsidePath, { recursive: true, force: true })
    +  })
    +})
    
  • packages/server/src/utilities/fileSystem/tests/filesystem.spec.ts+42 0 added
    @@ -0,0 +1,42 @@
    +import fs from "fs"
    +import path from "path"
    +import { budibaseTempDir } from "../../budibaseDir"
    +import {
    +  createTempFolder,
    +  deleteFolderFileSystem,
    +  extractTarball,
    +} from "../filesystem"
    +
    +describe("filesystem", () => {
    +  describe("createTempFolder", () => {
    +    it("should keep temporary folders inside the Budibase temp directory", () => {
    +      const folder = createTempFolder("../../../etc/cron.d/plugin")
    +
    +      expect(folder.startsWith(`${budibaseTempDir()}${path.sep}`)).toBe(true)
    +      expect(folder).not.toContain(`${path.sep}etc${path.sep}cron.d`)
    +      expect(fs.existsSync(folder)).toBe(true)
    +
    +      deleteFolderFileSystem(folder)
    +    })
    +  })
    +
    +  describe("extractTarball", () => {
    +    it("should reject extraction targets outside the Budibase temp directory", async () => {
    +      const outsidePath = path.resolve(budibaseTempDir(), "..", "outside")
    +
    +      await expect(
    +        extractTarball("/tmp/does-not-matter.tar.gz", outsidePath)
    +      ).rejects.toThrow("Path must be within the Budibase temp directory.")
    +    })
    +  })
    +
    +  describe("deleteFolderFileSystem", () => {
    +    it("should reject deleting paths outside the Budibase temp directory", () => {
    +      const outsidePath = path.resolve(budibaseTempDir(), "..", "outside")
    +
    +      expect(() => deleteFolderFileSystem(outsidePath)).toThrow(
    +        "Path must be within the Budibase temp directory."
    +      )
    +    })
    +  })
    +})
    
  • packages/types/src/documents/global/plugin.ts+1 0 modified
    @@ -55,6 +55,7 @@ export interface PluginMetadata {
     export interface PluginUpload {
       metadata: PluginMetadata
       directory: string
    +  cleanupDirectory: string
     }
     
     export interface PluginOrigin {
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.