Skip to content

feat: add FTP, SFTP and more backup destinations via rclone#4045

Open
promisingcoder wants to merge 52 commits intoDokploy:mainfrom
promisingcoder:bounty/416-backup-destinations
Open

feat: add FTP, SFTP and more backup destinations via rclone#4045
promisingcoder wants to merge 52 commits intoDokploy:mainfrom
promisingcoder:bounty/416-backup-destinations

Conversation

@promisingcoder
Copy link

@promisingcoder promisingcoder commented Mar 20, 2026

Summary

Extends the backup destination system to support FTP, SFTP, and other rclone-supported providers beyond just S3.

Fixes #416

/claim #416

Changes

  • Added new destination types to the schema
  • Extended rclone configuration for multiple providers
  • Added UI for configuring new destinations
  • Updated backup/restore utilities
  • Existing S3 backups unchanged

Greptile Summary

This PR extends Dokploy's backup destination system to support FTP, SFTP, and potentially other rclone-backed providers beyond S3. The core change adds a destinationType discriminator to the destination schema, introduces getFtpCredentials/getSftpCredentials helpers that mirror the existing getS3Credentials pattern, replaces all getS3Credentials call-sites with a new getDestinationCredentials dispatcher, and wires up the new destination types through the UI, router, and all backup/restore utilities.

Key concerns:

  • Critical security vulnerability — command injection via FTP/SFTP password: Both getFtpCredentials and getSftpCredentials (and the identical inline code in testConnection) embed the raw password into a single-quoted shell subcommand: --ftp-pass="$(rclone obscure '${ftpPassword}')". A password containing a single quote ' breaks the shell quoting boundary and could allow arbitrary command execution on the server. This affects every FTP/SFTP backup, restore, and connection-test code path. The fix is to pre-compute the obscured password in a separate execAsync call before building the rclone flags string, so no raw user input ever appears inside a shell-interpolated command.
  • Inconsistent needsShell guard in restore operations: Backup operations conditionally pass { shell: '/bin/bash' } to execAsync for FTP/SFTP, but all restore files (postgres.ts, mysql.ts, mariadb.ts, mongo.ts, compose.ts) call execAsync without this guard. In practice /bin/sh supports $(), but aligning the restore paths with the backup paths avoids confusion and potential issues on minimal systems.
  • Unused variable in handle-destinations.tsx: defaultPort (line 309) is computed but never referenced.

Confidence Score: 1/5

  • Not safe to merge — a command injection vulnerability allows arbitrary shell execution on the host when FTP/SFTP passwords contain single quotes.
  • The FTP/SFTP password is interpolated directly inside a single-quoted shell subcommand ($(rclone obscure '${ftpPassword}')). This is present in getFtpCredentials, getSftpCredentials, and the testConnection router — covering every backup, restore, and connection-test code path for FTP/SFTP destinations. Any user-supplied password containing a single quote breaks shell quoting and can inject arbitrary commands running as the dokploy service account.
  • packages/server/src/utils/backups/utils.ts (lines 83–96) and apps/dokploy/server/api/routers/destination.ts (lines 54–75) require immediate attention for the command injection fix.

Comments Outside Diff (1)

  1. packages/server/src/utils/restore/postgres.ts, line 41-45 (link)

    P2 Missing shell option for FTP/SFTP restore

    For backup operations the code conditionally passes { shell: '/bin/bash' } when the destination is FTP or SFTP (so that $(rclone obscure ...) subshell expansion works). This needsShell guard is absent here and in the other restore files (mariadb.ts, mysql.ts, mongo.ts, compose.ts).

    While Node.js's default /bin/sh does support POSIX $() syntax, the inconsistency is a maintenance hazard and may break on minimal systems where /bin/sh is dash or busybox and the command pipeline uses features that differ from bash.

    const destinationType = destination.destinationType || "s3";
    const needsShell = destinationType === "ftp" || destinationType === "sftp";
    
    if (serverId) {
      await execAsyncRemote(serverId, command);
    } else {
      await execAsync(command, needsShell ? { shell: "/bin/bash" } : undefined);
    }

    Apply the same pattern in all other restore files.

Last reviewed commit: "feat: add FTP and SF..."

Greptile also left 3 inline comments on this PR.

(5/5) You can turn off certain types of comments like style here!

cucumber-sp and others added 30 commits February 13, 2026 19:14
Add tag management system that allows users to create, edit, and delete
tags scoped to their organization, and assign them to projects for
better organization and filtering.

- Add tag and project_tag database schemas with Drizzle migration
- Add tRPC router for tag CRUD and project-tag assignment operations
- Add tag management page in Settings with color picker
- Add tag selector to project create/edit form
- Add tag filter to project list with localStorage persistence
- Display tag badges on project cards
Show only the count inside the filter button instead of rendering
individual tag badges alongside it.
Remove variant="blank" (forced h-4) and flex-1 (full width stretch)
to match the tag badge appearance from the settings page.
Replace duplicated inline badge styling with a reusable TagBadge
component to ensure consistent appearance across all tag displays.
Remove deleted tag IDs from the selected filter state when the
available tags list changes.
switchers

- Create AdvanceBreadcrumb component with searchable dropdowns
- Add project selector with environment expansion support
- Add service selector for quick switching between services
- Add environment selector badge for multi-environment projects
- Replace BreadcrumbSidebar with new component across all service pages
- Update projects page, environment page, and all service detail pages
  (application, compose, postgres, mysql, mariadb, redis, mongo)
…utilizing URL query parameters for ID retrieval
Enable file upload deployments via the public API, unlocking CI/CD workflows
similar to `railway up`. Users can now programmatically deploy by uploading
zip archives.

Depends on: Dokploy/trpc-openapi multipart/form-data support
Switch from z.instanceof(FormData) to uploadFileSchema (zod-form-data)
so the OpenAPI generator produces a proper multipart/form-data spec
with typed fields (zip as binary, applicationId, dropBuildPath).

Regenerate openapi.json with the drop-deployment endpoint included.
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
- Added health check functionality for PostgreSQL, Redis, and Traefik services before updating the web server.
- Introduced a modal state management system to guide users through the verification and update process.
- Updated UI components to display service health status and relevant messages during the update workflow.
- Refactored the update server button to reflect the latest version and availability of updates.
…ess-to-validate-dokploy-services

feat: enhance web server update process with health checks
- Bumped the version of @dokploy/trpc-openapi in both package.json and pnpm-lock.yaml.
- Removed unnecessary metadata from the dropDeployment procedure in application.ts.
…ent-api

feat: expose drop deployment endpoint in public API
Siumauricio and others added 20 commits March 19, 2026 00:43
- Changed input class from "focus:ring-0" to "focus-visible:ring-0" for improved accessibility and visual feedback on focus.
…cher

feat(ui): Add Vercel-style breadcrumb navigation with project and service switchers
…-unhandled-rejection

fix: prevent unhandled rejection in trustedOrigins on DB failure
…raints

- Introduced new SQL migration to create 'project_tag' and 'tag' tables.
- Added unique constraints and foreign key relationships to ensure data integrity.
- Updated journal and snapshot metadata to reflect the new migration.
- Added a new HandleTag component to manage tag creation and updates with validation.
- Integrated color selection and real-time preview for tags.
- Updated tag management references in TagFilter and TagSelector components to use the new HandleTag component.
- Integrated user permissions for tag creation, updating, and deletion in the TagManager component.
- Updated API routes to enforce permission checks for tag operations.
- Added new permissions for managing tags in the roles configuration.
- Improved error handling for unauthorized access in tag-related operations.
- Replaced role-based access control with permission-based checks for tag visibility in the side menu.
- Updated API route handlers to utilize protected procedures for tag queries, enhancing security and consistency in permission management.
- Added error handling for user permission fetching in the server-side props.
- Implemented a check for tag read permissions, redirecting unauthorized users to the home page.
- Enhanced the overall structure of the server-side logic for better clarity and maintainability.
- Simplified the payload construction for tag creation and updates in the HandleTag component.
- Improved code readability by consolidating the conditional logic for tagId handling.
feat: add project tags for organizing services
- Upgraded 'next' version from 16.1.6 to 16.2.0 in both pnpm-lock.yaml and package.json.
- Updated related dependency versions for '@trpc/next' and '@trpc/react-query' to align with the new 'next' version.
- Adjusted version hashes for better consistency in '@better-auth' packages.
Adds FTP and SFTP as new backup destination types alongside existing S3.
Includes schema migration, API routes, upload/download logic for both protocols,
restore support for all backup types, volume backup support, and UI forms
for credential management.

Closes Dokploy#416
@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. enhancement New feature or request labels Mar 20, 2026
Comment on lines +83 to +87
`--ftp-host="${ftpHost}"`,
`--ftp-port="${ftpPort || 21}"`,
`--ftp-user="${ftpUser}"`,
`--ftp-pass="$(rclone obscure '${ftpPassword}')"`,
];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Shell command injection via FTP/SFTP password

The ftpPassword is interpolated directly into a shell command using single-quote quoting:

--ftp-pass="$(rclone obscure '${ftpPassword}')"

If the password contains a single quote ('), the quoting breaks, producing malformed shell syntax — or worse, arbitrary command injection. For example, a password like '; cat /etc/passwd; echo ' would be interpreted by the shell as three separate statements.

The same issue exists in getSftpCredentials on line 96.

The safe approach is to pre-compute the obscured password in a separate step before building the command string:

export const getFtpCredentials = async (destination: Destination) => {
  const { ftpHost, ftpPort, ftpUser, ftpPassword } = destination;
  // Pre-obscure the password so it is never shell-interpolated raw
  const { stdout } = await execAsync(`rclone obscure ${JSON.stringify(ftpPassword)}`);
  const obscuredPass = stdout.trim();
  return [
    `--ftp-host="${ftpHost}"`,
    `--ftp-port="${ftpPort || 21}"`,
    `--ftp-user="${ftpUser}"`,
    `--ftp-pass="${obscuredPass}"`,
  ];
};

Alternatively, consider writing an rclone config file instead of passing credentials as CLI flags.

Comment on lines +54 to +75
const { ftpHost, ftpPort, ftpUser, ftpPassword, ftpBasePath } = input;
const rcloneFlags = [
`--ftp-host="${ftpHost}"`,
`--ftp-port="${ftpPort || 21}"`,
`--ftp-user="${ftpUser}"`,
`--ftp-pass="$(rclone obscure '${ftpPassword}')"`,
"--retries 1",
"--low-level-retries 1",
"--timeout 10s",
"--contimeout 5s",
];
const rcloneDestination = `:ftp:${ftpBasePath || "/"}`;
rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}" --max-depth 1`;
} else if (input.destinationType === "sftp") {
const { ftpHost, ftpPort, ftpUser, ftpPassword, ftpBasePath } = input;
const rcloneFlags = [
`--sftp-host="${ftpHost}"`,
`--sftp-port="${ftpPort || 22}"`,
`--sftp-user="${ftpUser}"`,
`--sftp-pass="$(rclone obscure '${ftpPassword}')"`,
"--retries 1",
"--low-level-retries 1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Same command injection on testConnection

The same raw password interpolation pattern appears in testConnection for both FTP (line 59) and SFTP (line 73):

`--ftp-pass="$(rclone obscure '${ftpPassword}')"`,
`--sftp-pass="$(rclone obscure '${ftpPassword}')"`,

A password containing a single quote breaks the shell quoting. The fix described in utils.ts (pre-compute the obscured password) should be applied here as well.

Comment on lines +309 to +310
const defaultPort = type === "sftp" ? 22 : 21;
const ftpPayload =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unused defaultPort variable

defaultPort is computed but never referenced — the FTP and SFTP payload objects below both hard-code their fallback ports directly (|| 21 and || 22).

Suggested change
const defaultPort = type === "sftp" ? 22 : 21;
const ftpPayload =
const ftpPayload =

promisingcoder and others added 2 commits March 20, 2026 21:30
- Replace shell command substitution `$(rclone obscure '...')` with
  execFileAsync("rclone", ["obscure", password]) so the password is
  passed as a direct argument, never interpolated into a shell string
- Use the pre-obscured value as a plain flag value --ftp-pass/--sftp-pass
- Apply fix in both utils.ts (getFtpCredentials, getSftpCredentials) and
  destination.ts testConnection handler
- Make getDestinationCredentials async and update all 7 callers with await
- Remove unused defaultPort variable in handle-destinations.tsx

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- P1: fix shell injection in FTP/SFTP credential flags by adding shellEscape
  helper that single-quotes values and escapes embedded single quotes,
  preventing host/user/pass from breaking out of shell command context
- P2: add sftpPrivateKey field to destination schema and DB table to support
  SFTP private key authentication (migration 0154), make ftpPassword optional
  on SFTP to allow key-only auth
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🙋 Bounty claim enhancement New feature or request size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants