From c429dc3455361dab181155d7e11dece7a1b6bbad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 29 Aug 2026 13:07:50 +0100 Subject: [PATCH] a remote path without a leading slash is relative to the remote home MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Requiring an absolute one was simply wrong. `scp file host:db-backups/fofos/` lands under the remote account's home — that is what every ssh tool does, and what anybody typing a destination expects, since the home directory is the one place they are certain to be able to write. What is still refused is unchanged and is the part that matters: the characters a shell would act on. This value IS interpreted remotely, since `ssh host mkdir -p ` runs through a shell, so they are rejected rather than escaped — refusing is checkable by reading the function, escaping is checkable only by being right about every shell. The summary shows a relative destination as ~/… because that is where it lands. Co-Authored-By: Claude Opus 5 --- api/backups.ts | 13 ++++++++----- web/BackupsRow.tsx | 9 ++++++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/api/backups.ts b/api/backups.ts index 851bb7b..629a4cb 100644 --- a/api/backups.ts +++ b/api/backups.ts @@ -113,16 +113,19 @@ export function checkHost(host: string): string { } /** - * An absolute path on the far side. + * A directory on the far side, absolute or relative. * - * This one IS interpreted by the remote shell — `ssh host mkdir -p /path` runs through it — so the - * characters a shell would act on are refused rather than escaped. Refusing is checkable by reading it; - * escaping is checkable by being right about every shell. + * RELATIVE IS NORMAL, and requiring a leading slash was simply wrong. `scp file host:db-backups/fofos/` + * lands under the remote account's home, which is what every ssh tool does and what anybody typing a + * destination expects — the home directory is the one place they are certain to be able to write. + * + * What is still refused: the characters a shell would act on. This value IS interpreted remotely — + * `ssh host mkdir -p ` runs through a shell — so they are rejected rather than escaped. Refusing + * is checkable by reading this function; escaping is checkable only by being right about every shell. */ export function checkRemotePath(path: string): string { const trimmed = path.trim(); if (!trimmed) throw new Error('A remote directory is required.'); - if (!trimmed.startsWith('/')) throw new Error('The remote directory must be an absolute path.'); if (/["'`$;&|<>()\\\s]/.test(trimmed)) { throw new Error('The remote directory may not contain spaces, quotes or shell characters.'); } diff --git a/web/BackupsRow.tsx b/web/BackupsRow.tsx index a2674b7..1db5095 100644 --- a/web/BackupsRow.tsx +++ b/web/BackupsRow.tsx @@ -175,7 +175,7 @@ export const BackupsRow = () => { {configured && settings.sshHost ? ( {' '} - → {settings.sshHost}:{settings.sshPath} + → {settings.sshHost}:{settings.sshPath?.startsWith('/') ? settings.sshPath : `~/${settings.sshPath}`} ) : null}

@@ -278,7 +278,7 @@ export const BackupsRow = () => { setSshPath(event.target.value)} - placeholder="/srv/backups/fofos" + placeholder="db-backups/fofos" className="w-full font-mono text-xs" autoComplete="off" spellCheck={false} @@ -296,7 +296,10 @@ export const BackupsRow = () => { {/* No password field, and there will not be one: this runs with nobody at a terminal, so it authenticates with the owner's SSH key and stores no credential of its own. */} -

Authenticated with this machine’s SSH key.

+

+ Authenticated with this machine’s SSH key. A path without a leading slash is relative to + the remote home. +