From 97aff10b7e7155431567fd6da1d3cd4a61517271 Mon Sep 17 00:00:00 2001 From: sl Date: Thu, 16 Jul 2026 21:06:02 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=9C=A8=20SSH=20=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E4=B8=8E=E9=9A=A7=E9=81=93=E7=9B=AE=E6=A0=87=E5=89=8D=E7=BB=88?= =?UTF-8?q?=E6=AD=A2=E9=80=89=E9=A1=B9=E8=A7=A3=E6=9E=90,=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E6=81=B6=E6=84=8F=E5=88=AB=E5=90=8D=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=20ProxyCommand=20=E7=AD=89=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SSH 目标来自用户配置,以 - 开头时会被 OpenSSH 当作命令行选项 - 统一加入 -- 可保留现有参数顺序,并同时覆盖普通命令和端口转发 --- packages/ssh/src/command.test.ts | 38 +++++++++++++++++++++++++++++++- packages/ssh/src/command.ts | 1 + packages/ssh/src/tunnel.test.ts | 12 ++++++++-- packages/ssh/src/tunnel.ts | 1 + 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/ssh/src/command.test.ts b/packages/ssh/src/command.test.ts index e5b621f87aa..af151aa6bde 100644 --- a/packages/ssh/src/command.test.ts +++ b/packages/ssh/src/command.test.ts @@ -8,7 +8,7 @@ import * as Result from "effect/Result"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import * as TestClock from "effect/testing/TestClock"; -import { ChildProcessSpawner } from "effect/unstable/process"; +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { baseSshArgs, @@ -99,6 +99,42 @@ describe("ssh command", () => { }), ); + it.effect("terminates SSH options before a user-controlled destination", () => { + let spawnedCommand: ChildProcess.Command | null = null; + const spawner = ChildProcessSpawner.make((command) => { + spawnedCommand = command; + return Effect.succeed(makeFailedProcess({ stdout: "", stderr: "Host lookup failed.\n" })); + }); + const processLayer = Layer.mergeAll( + NodeServices.layer, + Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner), + ); + + return Effect.gen(function* () { + yield* runSshCommand( + { + alias: "-oProxyCommand=malicious-command", + hostname: "-oProxyCommand=malicious-command", + username: null, + port: null, + }, + { remoteCommandArgs: ["sh", "-s"] }, + ).pipe(Effect.flip); + + assert.isNotNull(spawnedCommand); + if (spawnedCommand !== null && ChildProcess.isStandardCommand(spawnedCommand)) { + const separatorIndex = spawnedCommand.args.indexOf("--"); + assert.isAtLeast(separatorIndex, 0); + assert.deepEqual(spawnedCommand.args.slice(separatorIndex), [ + "--", + "-oProxyCommand=malicious-command", + "sh", + "-s", + ]); + } + }).pipe(Effect.provide(processLayer)); + }); + it.effect("resolves the remote t3 package spec from the desktop release channel", () => Effect.sync(() => { assert.equal( diff --git a/packages/ssh/src/command.ts b/packages/ssh/src/command.ts index 10927b43089..2397a7c71e7 100644 --- a/packages/ssh/src/command.ts +++ b/packages/ssh/src/command.ts @@ -200,6 +200,7 @@ const runSshCommandInScope = Effect.fn("ssh/command.runSshCommand.inScope")(func batchMode: input.batchMode ?? (input.interactiveAuth ? "no" : "yes"), }), ...(input.preHostArgs ?? []), + "--", hostSpec, ...(input.remoteCommandArgs ?? []), ]; diff --git a/packages/ssh/src/tunnel.test.ts b/packages/ssh/src/tunnel.test.ts index 7e7a5a54276..4d4ee3f4749 100644 --- a/packages/ssh/src/tunnel.test.ts +++ b/packages/ssh/src/tunnel.test.ts @@ -351,7 +351,11 @@ describe("ssh tunnel scripts", () => { tunnelKillCount += 1; }); } - if (args.includes("sh") && args.includes("--")) { + if ( + args.some( + (arg, index) => arg === "sh" && args[index + 1] === "-s" && args[index + 2] === "--", + ) + ) { return makeSuccessfulProcess('{"remotePort":3773}\n'); } if (args.includes("sh")) { @@ -388,7 +392,11 @@ describe("ssh tunnel scripts", () => { yield* manager.ensureEnvironment(target); - assert.equal(spawnedCommands.filter((args) => args.includes("-N")).length, 2); + const tunnelCommands = spawnedCommands.filter((args) => args.includes("-N")); + assert.equal(tunnelCommands.length, 2); + for (const args of tunnelCommands) { + assert.deepEqual(args.slice(-2), ["--", "julius@devbox"]); + } assert.equal(tunnelKillCount, 1); }).pipe(Effect.provide(layer), Effect.scoped); }); diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index 016d5e9a854..a56874be0e8 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -970,6 +970,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input: "-N", "-L", `${input.localPort}:127.0.0.1:${input.remotePort}`, + "--", hostSpec, ]; const sshCommand = yield* resolveSshCommand;