diff --git a/src/OpenClaw.Shared/Capabilities/SystemCapability.cs b/src/OpenClaw.Shared/Capabilities/SystemCapability.cs index 337e7e0..91d0e1a 100644 --- a/src/OpenClaw.Shared/Capabilities/SystemCapability.cs +++ b/src/OpenClaw.Shared/Capabilities/SystemCapability.cs @@ -647,6 +647,16 @@ public class SystemCapability : NodeCapabilityBase { if (normalized.Contains(dangerous, StringComparison.Ordinal)) return $"Dangerous allow rule is not permitted: {pattern}"; + + // Also block stem+wildcard (e.g. "rm*" bypasses "rm " because the + // fragment has a trailing space that the wildcard replaces). + var stem = dangerous.TrimEnd(); + if (stem.Length < dangerous.Length && + (normalized.Contains(stem + "*", StringComparison.Ordinal) || + normalized.Contains(stem + "?", StringComparison.Ordinal))) + { + return $"Dangerous allow rule is not permitted: {pattern}"; + } } } diff --git a/tests/OpenClaw.Shared.Tests/ExecApprovalPolicyTests.cs b/tests/OpenClaw.Shared.Tests/ExecApprovalPolicyTests.cs index 85613f4..57498c7 100644 --- a/tests/OpenClaw.Shared.Tests/ExecApprovalPolicyTests.cs +++ b/tests/OpenClaw.Shared.Tests/ExecApprovalPolicyTests.cs @@ -654,6 +654,52 @@ public class SystemCapabilityExecApprovalsTests } } + /// + /// Verifies that exec-approvals.set rejects Allow rules where a dangerous command stem + /// is immediately followed by a wildcard (e.g. "rm*"), which would bypass the trailing- + /// space fragment check used for patterns like "rm ". + /// + [Theory] + [InlineData("rm*")] + [InlineData("rm?")] + [InlineData("del*")] + [InlineData("del?")] + [InlineData("remove-item*")] + [InlineData("shutdown*")] + [InlineData("net*")] + public async Task ExecApprovalsSet_RejectsDangerousStemPlusWildcardAllowRule(string dangerousPattern) + { + var tempDir = Path.Combine(Path.GetTempPath(), $"test-{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempDir); + try + { + var policy = new ExecApprovalPolicy(tempDir, _logger); + var cap = CreateCapability(policy); + + var json = JsonDocument.Parse($@"{{ + ""baseHash"": ""{policy.GetPolicyHash()}"", + ""rules"": [ + {{""pattern"": ""{dangerousPattern}"", ""action"": ""allow""}} + ], + ""defaultAction"": ""deny"" + }}"); + + var request = new NodeInvokeRequest + { + Command = "system.execApprovals.set", + Args = json.RootElement + }; + + var result = await cap.ExecuteAsync(request); + Assert.False(result.Ok); + Assert.Contains("Dangerous allow rule is not permitted", result.Error!, StringComparison.OrdinalIgnoreCase); + } + finally + { + try { Directory.Delete(tempDir, true); } catch { } + } + } + [Fact] public async Task ExecApprovalsGet_ReturnsPolicy() {