feat: add FTP, SFTP and more backup destinations via rclone#4045
feat: add FTP, SFTP and more backup destinations via rclone#4045promisingcoder wants to merge 52 commits intoDokploy:mainfrom
Conversation
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)
cache invalidation logic
…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>
… rejection on DB failure
- 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
- 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
| `--ftp-host="${ftpHost}"`, | ||
| `--ftp-port="${ftpPort || 21}"`, | ||
| `--ftp-user="${ftpUser}"`, | ||
| `--ftp-pass="$(rclone obscure '${ftpPassword}')"`, | ||
| ]; |
There was a problem hiding this comment.
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.
| 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", |
There was a problem hiding this comment.
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.
| const defaultPort = type === "sftp" ? 22 : 21; | ||
| const ftpPayload = |
There was a problem hiding this comment.
- 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
Summary
Extends the backup destination system to support FTP, SFTP, and other rclone-supported providers beyond just S3.
Fixes #416
/claim #416
Changes
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
destinationTypediscriminator to thedestinationschema, introducesgetFtpCredentials/getSftpCredentialshelpers that mirror the existinggetS3Credentialspattern, replaces allgetS3Credentialscall-sites with a newgetDestinationCredentialsdispatcher, and wires up the new destination types through the UI, router, and all backup/restore utilities.Key concerns:
getFtpCredentialsandgetSftpCredentials(and the identical inline code intestConnection) 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 separateexecAsynccall before building the rclone flags string, so no raw user input ever appears inside a shell-interpolated command.needsShellguard in restore operations: Backup operations conditionally pass{ shell: '/bin/bash' }toexecAsyncfor FTP/SFTP, but all restore files (postgres.ts,mysql.ts,mariadb.ts,mongo.ts,compose.ts) callexecAsyncwithout this guard. In practice/bin/shsupports$(), but aligning the restore paths with the backup paths avoids confusion and potential issues on minimal systems.handle-destinations.tsx:defaultPort(line 309) is computed but never referenced.Confidence Score: 1/5
$(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.Comments Outside Diff (1)
packages/server/src/utils/restore/postgres.ts, line 41-45 (link)For backup operations the code conditionally passes
{ shell: '/bin/bash' }when the destination is FTP or SFTP (so that$(rclone obscure ...)subshell expansion works). ThisneedsShellguard is absent here and in the other restore files (mariadb.ts,mysql.ts,mongo.ts,compose.ts).While Node.js's default
/bin/shdoes support POSIX$()syntax, the inconsistency is a maintenance hazard and may break on minimal systems where/bin/shisdashorbusyboxand the command pipeline uses features that differ from bash.Apply the same pattern in all other restore files.
Last reviewed commit: "feat: add FTP and SF..."
(5/5) You can turn off certain types of comments like style here!