Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/OpenClaw.SetupEngine/ConfigureGatewayStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,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}
""";

Expand Down
106 changes: 105 additions & 1 deletion tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4102,7 +4102,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]
Expand Down Expand Up @@ -5165,6 +5165,110 @@ public void ConfigureGateway_QuotesExtraConfigValueWithEmbeddedSingleQuote()
commands);
}

// 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()
{
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);
}

// 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<string, string>
{
["gateway.reload.mode"] = reloadMode,
},
},
},
runner);
ctx.DistroName = "test-distro";
return ctx;
}

[Fact]
public async Task ConfigureGateway_UsesExtendedTimeoutForWslConfig()
{
Expand Down
Loading