From 0e6d12f3ffa8a2d9a65d79c1876062cb6f269511 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Tue, 22 Sep 2026 19:06:38 -0700 Subject: [PATCH 1/2] fix(setup): quote gateway bind, auth mode, and reload mode Signed-off-by: Sebastien Tardif --- .../ConfigureGatewayStep.cs | 9 ++-- .../SetupStepsTests.cs | 44 ++++++++++++++++++- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/OpenClaw.SetupEngine/ConfigureGatewayStep.cs b/src/OpenClaw.SetupEngine/ConfigureGatewayStep.cs index f09ac6215..9ac537730 100644 --- a/src/OpenClaw.SetupEngine/ConfigureGatewayStep.cs +++ b/src/OpenClaw.SetupEngine/ConfigureGatewayStep.cs @@ -89,7 +89,8 @@ public override async Task ExecuteAsync(SetupContext ctx, Cancellati """; var timeout = ComputeConfigurationTimeout(configCommands); - var result = await ctx.Commands.RunInWslAsync(distro, script, timeout, env, ct); + var result = await ctx.Commands.RunInWslAsync( + distro, script, timeout, env, ct, inputViaStdin: true); if (result.ExitCode != 0 || !result.Stdout.Contains("GATEWAY_CONFIGURED")) { @@ -124,10 +125,10 @@ internal static string BuildConfigCommands( openclaw plugins registry --refresh openclaw config set gateway.mode local openclaw config set gateway.port {port} - openclaw config set gateway.bind {gw.Bind} - openclaw config set gateway.auth.mode {gw.AuthMode} + openclaw config set gateway.bind {WslShellQuoting.QuotePosixSingleQuote(gw.Bind)} + openclaw config set gateway.auth.mode {WslShellQuoting.QuotePosixSingleQuote(gw.AuthMode)} openclaw config set gateway.auth.token "$OPENCLAW_GATEWAY_TOKEN" - openclaw config set gateway.reload.mode {gw.ReloadMode} + openclaw config set gateway.reload.mode {WslShellQuoting.QuotePosixSingleQuote(gw.ReloadMode)} openclaw config set {nodeCommandsAllowKey} {escapedAllowedCommands} """; diff --git a/tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs b/tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs index 3a568a07f..ddcea6645 100644 --- a/tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs +++ b/tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs @@ -3986,7 +3986,7 @@ public void ConfigureGateway_DefaultsReloadModeToHybrid() 18789, "'[]'"); - Assert.Contains("openclaw config set gateway.reload.mode hybrid", commands); + Assert.Contains("openclaw config set gateway.reload.mode 'hybrid'", commands); } [Fact] @@ -4874,6 +4874,48 @@ public void ConfigureGateway_QuotesExtraConfigValueWithEmbeddedSingleQuote() commands); } + // A bind, auth mode, or reload mode that contains a semicolon or $PATH must be one + // POSIX single-quoted token. The script also exports $PATH, so wsl.exe must not see + // it on the bash -c argv path. + [Fact] + public async Task ConfigureGateway_QuotesBindAndPipesScriptThroughStdin() + { + const string bind = "lan;$PATH"; + var commands = ConfigureGatewayStep.BuildConfigCommands( + new GatewayConfig + { + Bind = bind, + AuthMode = "token;$(id)", + ReloadMode = "hybrid;$PATH", + }, + 18789, + "'[]'"); + + Assert.Contains("openclaw config set gateway.bind 'lan;$PATH'", commands); + Assert.Contains("openclaw config set gateway.auth.mode 'token;$(id)'", commands); + Assert.Contains("openclaw config set gateway.reload.mode 'hybrid;$PATH'", commands); + Assert.DoesNotContain("openclaw config set gateway.bind lan;$PATH", commands); + + var runner = new FakeCommandRunner( + _ => Ok(), + (_, _, _) => Ok("GATEWAY_CONFIGURED")); + var ctx = CreateContext( + new SetupConfig + { + Gateway = new GatewayConfig { Bind = "loopback" }, + }, + runner); + ctx.DistroName = "test-distro"; + + var result = await new ConfigureGatewayStep().ExecuteAsync(ctx, CancellationToken.None); + + Assert.True(result.IsSuccess, result.Message); + var call = Assert.Single(runner.WslCalls); + Assert.True(call.InputViaStdin); + Assert.Contains("$PATH", call.Command); + Assert.Contains("openclaw config set gateway.bind 'loopback'", call.Command); + } + [Fact] public async Task ConfigureGateway_UsesExtendedTimeoutForWslConfig() { From 47e860bb02ea3e2c310716da33f40443b6f6cd17 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Wed, 23 Sep 2026 12:58:27 -0700 Subject: [PATCH 2/2] test(setup): cover a hostile gateway reload mode ExtraConfig reload mode is the unquoted value that can still reach the command. ExecuteAsync now rejects or quotes a reload mode that contains shell metacharacters. Signed-off-by: Sebastien Tardif --- .../SetupStepsTests.cs | 68 ++++++++++++++++++- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs b/tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs index ddcea6645..6ba4e248b 100644 --- a/tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs +++ b/tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs @@ -4874,9 +4874,11 @@ public void ConfigureGateway_QuotesExtraConfigValueWithEmbeddedSingleQuote() commands); } - // A bind, auth mode, or reload mode that contains a semicolon or $PATH must be one - // POSIX single-quoted token. The script also exports $PATH, so wsl.exe must not see - // it on the bash -c argv path. + // Bind is allowlisted in ExecuteAsync before BuildConfigCommands, so a hostile bind + // never reaches the script. Auth mode and reload mode are not allowlisted. The + // ExtraConfig gateway.reload.mode override is unvalidated and is the value that can + // still reach the command. Those values must be one POSIX single-quoted token. + // The script also exports $PATH, so wsl.exe must not see it on the bash -c argv path. [Fact] public async Task ConfigureGateway_QuotesBindAndPipesScriptThroughStdin() { @@ -4916,6 +4918,66 @@ public async Task ConfigureGateway_QuotesBindAndPipesScriptThroughStdin() Assert.Contains("openclaw config set gateway.bind 'loopback'", call.Command); } + // ExtraConfig gateway.reload.mode is not allowlisted. A value with shell + // metacharacters must be quoted, or the step must reject it before WSL runs. + // Either way the metacharacters must not change the command that runs. + [Fact] + public async Task ConfigureGateway_ExecuteAsyncQuotesOrRejectsHostileReloadMode() + { + const string safeReload = "off"; + const string hostileReload = "hybrid;$(id); echo PWNED"; + + var safeRunner = new FakeCommandRunner( + _ => Ok(), + (_, _, _) => Ok("GATEWAY_CONFIGURED")); + var safeResult = await new ConfigureGatewayStep().ExecuteAsync( + CreateReloadContext(safeRunner, safeReload), + CancellationToken.None); + Assert.True(safeResult.IsSuccess, safeResult.Message); + var safeCall = Assert.Single(safeRunner.WslCalls); + + var hostileRunner = new FakeCommandRunner( + _ => Ok(), + (_, _, _) => Ok("GATEWAY_CONFIGURED")); + var hostileResult = await new ConfigureGatewayStep().ExecuteAsync( + CreateReloadContext(hostileRunner, hostileReload), + CancellationToken.None); + + if (!hostileResult.IsSuccess) + { + Assert.Empty(hostileRunner.WslCalls); + return; + } + + var hostileCall = Assert.Single(hostileRunner.WslCalls); + var quotedHostile = "'" + hostileReload + "'"; + var quotedSafe = "'" + safeReload + "'"; + Assert.Contains($"openclaw config set gateway.reload.mode {quotedHostile}", hostileCall.Command); + Assert.DoesNotContain($"openclaw config set gateway.reload.mode {hostileReload}", hostileCall.Command); + Assert.Equal( + safeCall.Command.Replace(quotedSafe, quotedHostile, StringComparison.Ordinal), + hostileCall.Command); + } + + private SetupContext CreateReloadContext(ICommandRunner runner, string reloadMode) + { + var ctx = CreateContext( + new SetupConfig + { + Gateway = new GatewayConfig + { + Bind = "loopback", + ExtraConfig = new Dictionary + { + ["gateway.reload.mode"] = reloadMode, + }, + }, + }, + runner); + ctx.DistroName = "test-distro"; + return ctx; + } + [Fact] public async Task ConfigureGateway_UsesExtendedTimeoutForWslConfig() {