From cffae53b43ab1ba41028d38d3fc9dafbc1721508 Mon Sep 17 00:00:00 2001 From: dwc1997 <66739829+dwc1997@users.noreply.github.com> Date: Thu, 14 May 2026 04:19:02 +0800 Subject: [PATCH] fix(security): classify broad Windows SIDs as world principals Carry Windows ACL world-principal classification through @openclaw/fs-safe@0.2.2 so Anonymous Logon, Guests, Interactive, Network, and Local SID/principal variants are treated as world-equivalent in filesystem audit findings. Also add regression coverage, changelog coverage, a narrow lint cleanup, and a UI test isolation fix needed by the current CI shard. Co-authored-by: dwc <118101032587@njust.edu.cn> --- CHANGELOG.md | 1 + patches/@openclaw__fs-safe@0.2.2.patch | 47 ++++++++++++++ pnpm-lock.yaml | 5 +- pnpm-workspace.yaml | 1 + src/security/audit-filesystem-windows.test.ts | 36 ++++++++++ src/security/windows-acl.test.ts | 65 +++++++++++++++++++ ui/src/ui/chat/grouped-render.test.ts | 8 +-- 7 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 patches/@openclaw__fs-safe@0.2.2.patch diff --git a/CHANGELOG.md b/CHANGELOG.md index 58b44593ebda..88b5c1d02101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Security/Windows ACL audit: classify Anonymous Logon, Guests, Interactive, Local, and Network SIDs as world-equivalent principals so broadly writable paths stay critical instead of being downgraded to group-writable. Fixes #74350. (#74383) Thanks @dwc1997. - Media-understanding: retry transient remote attachment fetch failures before audio or vision processing, so Discord voice notes are not lost after one network/CDN blip. Fixes #74316. Thanks @vyctorbrzezowski and @gabrielexito-stack. - Control UI: order timestamped live stream and tool items before untimestamped history fallbacks, keeping chat history in visible time order. Fixes #80759. (#81016) Thanks @akrimm702. - iMessage: stop sending visible `` placeholder text for media-only native image sends while preserving the internal echo key that prevents self-echo duplicate replies. (#81209) Thanks @homer-byte. diff --git a/patches/@openclaw__fs-safe@0.2.2.patch b/patches/@openclaw__fs-safe@0.2.2.patch new file mode 100644 index 000000000000..44c54023260d --- /dev/null +++ b/patches/@openclaw__fs-safe@0.2.2.patch @@ -0,0 +1,47 @@ +diff --git a/dist/permissions.js b/dist/permissions.js +index 67df110..445b04d 100644 +--- a/dist/permissions.js ++++ b/dist/permissions.js +@@ -12,6 +12,15 @@ const WORLD_PRINCIPALS = new Set([ + "builtin\\users", + "authenticated users", + "nt authority\\authenticated users", ++ "anonymous logon", ++ "nt authority\\anonymous logon", ++ "guests", ++ "builtin\\guests", ++ "interactive", ++ "nt authority\\interactive", ++ "network", ++ "nt authority\\network", ++ "local", + ]); + const TRUSTED_BASE = new Set([ + "nt authority\\system", +@@ -31,7 +40,16 @@ const TRUSTED_SIDS = new Set([ + "s-1-5-32-544", + "s-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464", + ]); +-const WORLD_SIDS = new Set(["s-1-1-0", "s-1-5-11", "s-1-5-32-545"]); ++const WORLD_SIDS = new Set([ ++ "s-1-1-0", ++ "s-1-5-11", ++ "s-1-5-32-545", ++ "s-1-5-7", ++ "s-1-5-32-546", ++ "s-1-5-4", ++ "s-1-2-0", ++ "s-1-5-2", ++]); + const STATUS_PREFIXES = [ + "successfully processed", + "processed", +@@ -202,6 +220,8 @@ function buildTrustedPrincipals(env) { + } + } + const userSid = normalizeSid(env?.USERSID ?? ""); ++ // Guard: never add any world-equivalent SID to the trusted set, even if ++ // USERSID is set to one by a malicious process. + if (userSid && SID_RE.test(userSid) && !WORLD_SIDS.has(userSid)) { + trusted.add(userSid); + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94ac32bf4224..0c4e9d8fdf7d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,7 @@ packageExtensionsChecksum: sha256-oc/FAHkBR844HBfph1RZWyRMHHBpIFya25tyv5SGf6s= patchedDependencies: '@agentclientprotocol/claude-agent-acp@0.33.1': 3995624bb834cc60fea1461c7ef33f1fcdd8fb58b8f43f2f1490bc689f6e1be2 + '@openclaw/fs-safe@0.2.2': 9068eacd1e87c234c1084b7c6855fce28d019506a084e711d137218c4d369313 baileys@7.0.0-rc11: a9aea1790d2c65b1ae543c77faca4119bbfb91ee3b6ca6c38d1cad4f5702ada2 importers: @@ -103,7 +104,7 @@ importers: version: 0.6.0 '@openclaw/fs-safe': specifier: 0.2.2 - version: 0.2.2 + version: 0.2.2(patch_hash=9068eacd1e87c234c1084b7c6855fce28d019506a084e711d137218c4d369313) '@slack/bolt': specifier: 4.7.2 version: 4.7.2(@types/express@5.0.6) @@ -9982,7 +9983,7 @@ snapshots: '@openai/codex@0.130.0-win32-x64': optional: true - '@openclaw/fs-safe@0.2.2': + '@openclaw/fs-safe@0.2.2(patch_hash=9068eacd1e87c234c1084b7c6855fce28d019506a084e711d137218c4d369313)': optionalDependencies: jszip: 3.10.1 tar: 7.5.15 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4916b3eadb0e..184e09b448dc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -95,5 +95,6 @@ peerDependencyRules: "prism-media>opusscript": "^0.0.8 || ^0.1.1" patchedDependencies: + "@openclaw/fs-safe@0.2.2": "patches/@openclaw__fs-safe@0.2.2.patch" "baileys@7.0.0-rc11": "patches/baileys@7.0.0-rc11.patch" "@agentclientprotocol/claude-agent-acp@0.33.1": "patches/@agentclientprotocol__claude-agent-acp@0.33.1.patch" diff --git a/src/security/audit-filesystem-windows.test.ts b/src/security/audit-filesystem-windows.test.ts index 8687a0e6c21a..e2c2709bda6d 100644 --- a/src/security/audit-filesystem-windows.test.ts +++ b/src/security/audit-filesystem-windows.test.ts @@ -85,6 +85,42 @@ describe("security audit filesystem Windows findings", () => { ), ).toBe(true); })(), + (async () => { + const tmp = await tempCases.makeTmpDir("win-anon-world"); + const stateDir = path.join(tmp, "state"); + await fs.mkdir(stateDir, { recursive: true }); + const configPath = path.join(stateDir, "openclaw.json"); + await fs.writeFile(configPath, "{}\n", "utf-8"); + const findings = await collectFilesystemFindings({ + stateDir, + configPath, + platform: "win32", + env: windowsAuditEnv, + execIcacls: async (_cmd: string, args: string[]) => { + const target = args[0]; + if (target.endsWith(`${path.sep}state`)) { + return { + stdout: `${target} *S-1-5-18:(F)\n *S-1-5-7:(F)\n`, + stderr: "", + }; + } + return { + stdout: `${target} *S-1-5-18:(F)\n DESKTOP-TEST\\Tester:(F)\n`, + stderr: "", + }; + }, + }); + expect( + findings.some( + (finding) => + finding.checkId === "fs.state_dir.perms_world_writable" && + finding.severity === "critical", + ), + ).toBe(true); + expect( + findings.some((finding) => finding.checkId === "fs.state_dir.perms_group_writable"), + ).toBe(false); + })(), ]); }); }); diff --git a/src/security/windows-acl.test.ts b/src/security/windows-acl.test.ts index de8e5e981710..0e70a012fb47 100644 --- a/src/security/windows-acl.test.ts +++ b/src/security/windows-acl.test.ts @@ -396,6 +396,71 @@ Successfully processed 1 files`; ], expected: { untrustedWorld: 1 }, }, + { + name: "Anonymous Logon SID (S-1-5-7) is world, not group", + entries: [ + aclEntry({ + principal: "*S-1-5-7", + rights: ["R"], + rawRights: "(R)", + canRead: true, + canWrite: false, + }), + ], + expected: { untrustedWorld: 1 }, + }, + { + name: "BUILTIN\\\\Guests SID (S-1-5-32-546) is world, not group", + entries: [ + aclEntry({ + principal: "*S-1-5-32-546", + rights: ["R"], + rawRights: "(R)", + canRead: true, + canWrite: false, + }), + ], + expected: { untrustedWorld: 1 }, + }, + { + name: "Interactive SID (S-1-5-4) is world, not group", + entries: [ + aclEntry({ + principal: "*S-1-5-4", + rights: ["R"], + rawRights: "(R)", + canRead: true, + canWrite: false, + }), + ], + expected: { untrustedWorld: 1 }, + }, + { + name: "Local SID (S-1-2-0) is world, not group", + entries: [ + aclEntry({ + principal: "*S-1-2-0", + rights: ["R"], + rawRights: "(R)", + canRead: true, + canWrite: false, + }), + ], + expected: { untrustedWorld: 1 }, + }, + { + name: "Network SID (S-1-5-2) is world, not group", + entries: [ + aclEntry({ + principal: "*S-1-5-2", + rights: ["R"], + rawRights: "(R)", + canRead: true, + canWrite: false, + }), + ], + expected: { untrustedWorld: 1 }, + }, ] as const)("$name", ({ entries, env, expected }) => { expectSummaryCounts(entries, expected, env); }); diff --git a/ui/src/ui/chat/grouped-render.test.ts b/ui/src/ui/chat/grouped-render.test.ts index d6b7ea3797e1..25d37805c2a4 100644 --- a/ui/src/ui/chat/grouped-render.test.ts +++ b/ui/src/ui/chat/grouped-render.test.ts @@ -1107,10 +1107,10 @@ describe("grouped chat rendering", () => { const objectUrl = "blob:managed-image"; vi.stubGlobal( "URL", - Object.assign(URL, { - createObjectURL: vi.fn(() => objectUrl), - revokeObjectURL: vi.fn(), - }), + class extends URL { + static override createObjectURL = vi.fn(() => objectUrl); + static override revokeObjectURL = vi.fn(); + }, ); const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => { const headers = init?.headers as Headers;