DiskPush runs a program that copies files between machines, with credentials, sometimes with a delete flag. This is the reasoning behind how it does that.
spawn('rsync', validatedArgs, { shell: false })Never:
exec(`rsync ${userInput}`)Every argument comes from a typed option, a parsed endpoint, or the user's
explicit pass-through list. Nothing is interpolated into a string that a shell
will read back. A directory named weird $(touch /tmp/pwned); \id` 'q' & dir`
is copied, with that name, and nothing executes. There is a test that asserts
exactly this against the real rsync binary.
ssh host command... always runs the command through the remote login shell,
joining argv with spaces first. Server-to-server orchestration cannot avoid
that shell, so DiskPush quotes for it: every token of the remote rsync command
is POSIX single-quoted (' becomes '\'') and the whole command is passed as
one argv element. This is the only place in the codebase permitted to build a
shell string, it is one small function, and it is tested with hostile paths.
This one is easy to miss. When rsync talks to a remote host, it historically
passed the remote path on the remote command line, where the remote login
shell expanded it. A path containing $(...) was therefore remote code
execution, in rsync itself, with no help from the client.
rsync 3.0.0 added --protect-args to send arguments through the protocol
instead. rsync 3.2.4 made that the default and renamed it --secluded-args.
DiskPush gates on the version:
| Local rsync | Behaviour |
|---|---|
| 3.2.4 or newer | Already the default. No flag added. |
| 3.0.0 to 3.2.3 | --protect-args passed explicitly. |
| Older than 3.0.0 | Not possible. DiskPush warns that paths with shell metacharacters are unsafe against that host. |
| Unknown | --protect-args passed anyway, so an old rsync fails loudly instead of silently handing the path to a shell. |
- Host keys are verified on every connection.
- A new host prompts once, showing the
SHA256:fingerprint, and is recorded on acceptance. - A changed host key blocks the connection. It is not a prompt. If the change was expected, the old entry has to be removed deliberately.
- A key marked
@revokedinknown_hostsis refused. - Both plain and HMAC-SHA1 hashed
known_hostsentries are understood, so a hashed file does not read as empty and make every host look new. StrictHostKeyChecking=accept-newis the weakest position DiskPush takes. There is no setting that disables host key checking globally.
- No passwords or key passphrases are stored in the local database. Look at
packages/database/src/migrations.ts: there is no column for them. - SSH agent authentication is preferred, and stores nothing at all.
- Where a secret must persist, it belongs in OS-backed secure storage, not in the SQLite file that a backup would sweep up.
- Credentials are never written to logs. "Copy command" redacts.
- No delete flag is generated by default, by any command except
mirror. - A delete-enabled job cannot be constructed as a live job until its dry run has been reviewed and confirmed. This is enforced in the argument builder, not in the UI, so every surface inherits it.
--deleteand friends in the pass-through section are refused. Passing them through would let asyncdelete files without ever showing the preview whose entire purpose is to show you that.--remove-source-filesis refused. DiskPush copies; it does not move.- Unattended mirroring is possible, per profile, and off by default.
Off by default, opt-in per connection.
While a forwarded session is open, anyone with root on the intermediate host can use the agent to authenticate as you, anywhere the agent's keys are accepted. That is a real and specific cost. For unattended server-to-server work, a dedicated restricted key on the source host is the better answer.
nodeIntegration: false,contextIsolation: true, sandboxed renderer.- The renderer gets no filesystem or process APIs, only narrowly scoped IPC
operations over
contextBridge. - Every IPC input is validated with Zod in the main process. Renderer-supplied paths, flags and IDs are treated as untrusted input, because a renderer compromise should not become a shell.
- Strict CSP, no
eval, no remote content, navigation restricted to app pages.
- It does not upload your files anywhere.
- It does not relay server-to-server payloads through the desktop or through any hosted service, and will not silently start.
- It does not phone home. Analytics on the website never receives connection names, hostnames, paths, filenames, or anything about a transfer.
Security issues: security@profullstack.com. Please do not open a public issue for anything exploitable.