Skip to content

Commit 5d695ab

Browse files
shanselmanCopilot
andcommitted
fix: omit unsupported MXC denied paths
Windows wxc-exec rejects filesystem.deniedPaths before launching the sandboxed command. Keep denied paths as the local allow-list filter, but omit the unsupported backend field so AppContainer default-deny can run the command while still stripping parent/exact grants over sensitive paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4f34d10 commit 5d695ab

6 files changed

Lines changed: 43 additions & 115 deletions

File tree

src/OpenClaw.Shared/Mxc/MxcConfigBuilder.cs

Lines changed: 5 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ internal static MxcConfig Build(
6565
if (request is null) throw new ArgumentNullException(nameof(request));
6666
if (string.IsNullOrWhiteSpace(scratchDir)) throw new ArgumentException("scratchDir required", nameof(scratchDir));
6767
if (context is null) throw new ArgumentNullException(nameof(context));
68-
var deniedPathExists = context.DeniedPathExists ?? PathExists;
6968
var readonlyGrantIsBackendSafe = context.ReadonlyGrantIsBackendSafe ?? IsBackendSafeReadonlyGrant;
7069

7170
var policy = request.Policy;
@@ -107,13 +106,11 @@ internal static MxcConfig Build(
107106
rwFromPolicy.Add(scratchDir);
108107

109108
// denied list from policy (settings dir, ~/.ssh, browser profiles, ...).
110-
// Use the full list for local allow-list filtering, but do not emit
111-
// known host profile roots to the MXC DACL fallback: those paths often
112-
// cannot be prepared and make the sandbox fail before command launch.
109+
// Keep the full list for local allow-list filtering, but do not emit
110+
// filesystem.deniedPaths to wxc-exec. Windows MXC 0.7 rejects that field;
111+
// omitted grants remain denied by default inside the AppContainer.
113112
var deniedForFiltering = (policy?.Filesystem?.DeniedPaths ?? Array.Empty<string>()).ToList();
114-
var deniedForBackend = deniedForFiltering
115-
.Where(path => ShouldEmitDeniedPathToBackend(path, deniedPathExists))
116-
.ToList();
113+
string[]? deniedForBackend = null;
117114

118115
// cwd auto-grant — AppContainer does not auto-grant the working
119116
// directory. Give ungranted cwd read access so shells can start, but
@@ -188,7 +185,7 @@ internal static MxcConfig Build(
188185
{
189186
ReadonlyPaths = roFromPolicy.ToArray(),
190187
ReadwritePaths = rwFromPolicy.ToArray(),
191-
DeniedPaths = deniedForBackend.ToArray(),
188+
DeniedPaths = deniedForBackend,
192189
// SDK output didn't include clearPolicyOnExit even when the
193190
// input policy had it set, so we omit it here too.
194191
ClearPolicyOnExit = null,
@@ -385,59 +382,6 @@ private static List<string> FilterOutDenied(List<string> allowed, List<string> d
385382
.ToList();
386383
}
387384

388-
private static bool ShouldEmitDeniedPathToBackend(string path, Func<string, bool> pathExists)
389-
{
390-
var normalized = NormalizePath(path);
391-
if (string.IsNullOrWhiteSpace(normalized))
392-
return false;
393-
394-
foreach (var hostProfileRoot in HostProfileDenyRoots())
395-
{
396-
var root = NormalizePath(hostProfileRoot);
397-
if (!string.IsNullOrWhiteSpace(root) && IsSameOrNested(normalized, root))
398-
return false;
399-
}
400-
401-
try
402-
{
403-
if (!pathExists(normalized))
404-
return false;
405-
}
406-
catch
407-
{
408-
return false;
409-
}
410-
411-
return true;
412-
}
413-
414-
private static bool PathExists(string path) => Directory.Exists(path) || File.Exists(path);
415-
416-
private static IEnumerable<string> HostProfileDenyRoots()
417-
{
418-
var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
419-
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
420-
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
421-
422-
if (!string.IsNullOrWhiteSpace(userProfile))
423-
{
424-
yield return Path.Combine(userProfile, ".ssh");
425-
}
426-
427-
if (!string.IsNullOrWhiteSpace(localAppData))
428-
{
429-
yield return Path.Combine(localAppData, "Google", "Chrome", "User Data");
430-
yield return Path.Combine(localAppData, "Microsoft", "Edge", "User Data");
431-
yield return Path.Combine(localAppData, "BraveSoftware", "Brave-Browser", "User Data");
432-
}
433-
434-
if (!string.IsNullOrWhiteSpace(appData))
435-
{
436-
yield return Path.Combine(appData, "Mozilla", "Firefox", "Profiles");
437-
yield return Path.Combine(appData, "Microsoft", "Windows", "PowerShell", "PSReadLine");
438-
}
439-
}
440-
441385
private static bool IsCoveredBy(string candidate, IEnumerable<string> ancestors)
442386
{
443387
var nc = NormalizePath(candidate);
@@ -538,7 +482,6 @@ private sealed record SystemRunArgs(string Command, string Shell, IReadOnlyList<
538482
internal sealed record MxcConfigBuildContext(
539483
string? ContainerId = null,
540484
string? PathEnvVar = null,
541-
Func<string, bool>? DeniedPathExists = null,
542485
Func<string, bool>? ReadonlyGrantIsBackendSafe = null)
543486
{
544487
public static MxcConfigBuildContext Default { get; } = new();

tests/OpenClaw.Shared.Tests/Mxc/Golden/sdk-config-balanced.json

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,6 @@
1414
"C:\\Golden\\Documents",
1515
"C:\\Golden\\Downloads",
1616
"C:\\Golden\\Desktop"
17-
],
18-
"deniedPaths": [
19-
"C:\\Golden\\Settings",
20-
"C:\\Golden\\.ssh",
21-
"C:\\Golden\\Chrome",
22-
"C:\\Golden\\Edge",
23-
"C:\\Golden\\Brave",
24-
"C:\\Golden\\Firefox",
25-
"C:\\Golden\\PSReadLine"
2617
]
2718
},
2819
"ui": {

tests/OpenClaw.Shared.Tests/Mxc/Golden/sdk-config-custom.json

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,6 @@
1313
],
1414
"readonlyPaths": [
1515
"C:\\Golden\\Documents"
16-
],
17-
"deniedPaths": [
18-
"C:\\Golden\\Settings",
19-
"C:\\Golden\\.ssh",
20-
"C:\\Golden\\Chrome",
21-
"C:\\Golden\\Edge",
22-
"C:\\Golden\\Brave",
23-
"C:\\Golden\\Firefox",
24-
"C:\\Golden\\PSReadLine"
2516
]
2617
},
2718
"ui": {

tests/OpenClaw.Shared.Tests/Mxc/Golden/sdk-config-locked-down.json

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,7 @@
1010
"readwritePaths": [
1111
"C:\\Golden\\Scratch"
1212
],
13-
"readonlyPaths": [],
14-
"deniedPaths": [
15-
"C:\\Golden\\Settings",
16-
"C:\\Golden\\.ssh",
17-
"C:\\Golden\\Chrome",
18-
"C:\\Golden\\Edge",
19-
"C:\\Golden\\Brave",
20-
"C:\\Golden\\Firefox",
21-
"C:\\Golden\\PSReadLine"
22-
]
13+
"readonlyPaths": []
2314
},
2415
"ui": {
2516
"disable": true,

tests/OpenClaw.Shared.Tests/Mxc/Golden/sdk-config-permissive.json

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,7 @@
1313
"C:\\Golden\\Desktop",
1414
"C:\\Golden\\Scratch"
1515
],
16-
"readonlyPaths": [],
17-
"deniedPaths": [
18-
"C:\\Golden\\Settings",
19-
"C:\\Golden\\.ssh",
20-
"C:\\Golden\\Chrome",
21-
"C:\\Golden\\Edge",
22-
"C:\\Golden\\Brave",
23-
"C:\\Golden\\Firefox",
24-
"C:\\Golden\\PSReadLine"
25-
]
16+
"readonlyPaths": []
2617
},
2718
"ui": {
2819
"disable": true,

tests/OpenClaw.Shared.Tests/Mxc/MxcConfigBuilderTests.cs

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ private static class P
4242
P.Settings, P.Ssh, P.Chrome, P.Edge, P.Brave, P.Firefox, P.PsRead,
4343
};
4444

45-
private static readonly Func<string, bool> DeniedPathExists = _ => true;
46-
4745
private static SandboxPolicy LockedDownPolicy() => new(
4846
Version: MxcPolicyBuilder.SupportedPolicyVersion,
4947
Filesystem: new FilesystemPolicy(
@@ -99,15 +97,13 @@ private static MxcConfig BuildConfig(
9997
string scratchDir = P.Scratch,
10098
string? containerId = null,
10199
string? pathEnvVar = "",
102-
Func<string, bool>? deniedPathExists = null,
103100
Func<string, bool>? readonlyGrantIsBackendSafe = null) =>
104101
MxcConfigBuilder.Build(
105102
request,
106103
scratchDir,
107104
new MxcConfigBuildContext(
108105
ContainerId: containerId,
109106
PathEnvVar: pathEnvVar,
110-
DeniedPathExists: deniedPathExists ?? DeniedPathExists,
111107
ReadonlyGrantIsBackendSafe: readonlyGrantIsBackendSafe));
112108

113109
private static string ExpectedSystemCmdExe()
@@ -295,7 +291,7 @@ public void Build_DoesNotAutoGrantCwd_WhenOverlapsDenied()
295291
}
296292

297293
[Fact]
298-
public void Build_FiltersHostProfileDeniedPathsBeforeBackendEmissionButStillFiltersAllows()
294+
public void Build_OmitsDeniedPathsBeforeBackendEmissionButStillFiltersAllows()
299295
{
300296
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
301297
if (string.IsNullOrWhiteSpace(localAppData))
@@ -323,34 +319,59 @@ public void Build_FiltersHostProfileDeniedPathsBeforeBackendEmissionButStillFilt
323319

324320
Assert.DoesNotContain(chromeProfile, config.Filesystem!.ReadwritePaths!, StringComparer.OrdinalIgnoreCase);
325321
Assert.DoesNotContain(userProfile, config.Filesystem.ReadwritePaths!, StringComparer.OrdinalIgnoreCase);
326-
Assert.DoesNotContain(chromeProfile, config.Filesystem.DeniedPaths!, StringComparer.OrdinalIgnoreCase);
327-
Assert.DoesNotContain(sshPath, config.Filesystem.DeniedPaths!, StringComparer.OrdinalIgnoreCase);
328-
Assert.Contains(settingsDeny, config.Filesystem.DeniedPaths!, StringComparer.OrdinalIgnoreCase);
322+
Assert.Null(config.Filesystem.DeniedPaths);
329323
}
330324

331325
[Fact]
332-
public void Build_FiltersMissingDeniedPathsBeforeBackendEmissionButStillFiltersAllows()
326+
public void Build_RemovesParentReadwriteGrantContainingDeniedSettingsDirectory_WhenDeniedPathsAreNotEmitted()
333327
{
334328
var parent = "C:\\Users\\example\\AppData\\Roaming";
335-
var missingSettingsDeny = Path.Combine(parent, "OpenClawTray");
329+
var settingsDeny = Path.Combine(parent, "OpenClawTray");
336330
var policy = new SandboxPolicy(
337331
Version: MxcPolicyBuilder.SupportedPolicyVersion,
338332
Filesystem: new FilesystemPolicy(
339-
ReadwritePaths: new[] { parent },
333+
ReadwritePaths: new[] { parent, settingsDeny },
340334
ReadonlyPaths: Array.Empty<string>(),
341-
DeniedPaths: new[] { missingSettingsDeny },
335+
DeniedPaths: new[] { settingsDeny },
342336
ClearPolicyOnExit: true),
343337
Network: new NetworkPolicy(false, false),
344338
Ui: new UiPolicy(false, ClipboardPolicy.None, false),
345339
TimeoutMs: 30_000);
346340

347341
var config = BuildConfig(
348342
RequestFor(policy),
349-
pathEnvVar: "",
350-
deniedPathExists: _ => false);
343+
pathEnvVar: "");
351344

352345
Assert.DoesNotContain(parent, config.Filesystem!.ReadwritePaths!, StringComparer.OrdinalIgnoreCase);
353-
Assert.DoesNotContain(missingSettingsDeny, config.Filesystem.DeniedPaths!, StringComparer.OrdinalIgnoreCase);
346+
Assert.DoesNotContain(settingsDeny, config.Filesystem.ReadwritePaths!, StringComparer.OrdinalIgnoreCase);
347+
Assert.DoesNotContain(settingsDeny, config.Filesystem.ReadonlyPaths!, StringComparer.OrdinalIgnoreCase);
348+
Assert.Null(config.Filesystem.DeniedPaths);
349+
}
350+
351+
[Fact]
352+
public void Build_RemovesParentReadonlyGrantContainingDeniedSettingsDirectory_WhenDeniedPathsAreNotEmitted()
353+
{
354+
var parent = "C:\\Users\\example\\AppData\\Roaming";
355+
var settingsDeny = Path.Combine(parent, "OpenClawTray");
356+
var policy = new SandboxPolicy(
357+
Version: MxcPolicyBuilder.SupportedPolicyVersion,
358+
Filesystem: new FilesystemPolicy(
359+
ReadwritePaths: Array.Empty<string>(),
360+
ReadonlyPaths: new[] { parent, settingsDeny },
361+
DeniedPaths: new[] { settingsDeny },
362+
ClearPolicyOnExit: true),
363+
Network: new NetworkPolicy(false, false),
364+
Ui: new UiPolicy(false, ClipboardPolicy.None, false),
365+
TimeoutMs: 30_000);
366+
367+
var config = BuildConfig(
368+
RequestFor(policy),
369+
pathEnvVar: "");
370+
371+
Assert.DoesNotContain(parent, config.Filesystem!.ReadonlyPaths!, StringComparer.OrdinalIgnoreCase);
372+
Assert.DoesNotContain(settingsDeny, config.Filesystem.ReadwritePaths!, StringComparer.OrdinalIgnoreCase);
373+
Assert.DoesNotContain(settingsDeny, config.Filesystem.ReadonlyPaths!, StringComparer.OrdinalIgnoreCase);
374+
Assert.Null(config.Filesystem.DeniedPaths);
354375
}
355376

356377
[Fact]

0 commit comments

Comments
 (0)