fix(test): use NTFS junctions and platform guards for symlink tests on Windows (openclaw#28747) thanks @arosstale

Verified:
- pnpm install --frozen-lockfile
- pnpm test src/agents/apply-patch.test.ts src/agents/sandbox/fs-bridge.test.ts src/agents/sandbox/validate-sandbox-security.test.ts src/infra/archive.test.ts

Co-authored-by: arosstale <117890364+arosstale@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Artale
2026-03-02 17:45:19 +01:00
committed by GitHub
parent 18f8393b6c
commit 1b462ed174
5 changed files with 41 additions and 9 deletions

View File

@@ -47,6 +47,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Gateway/Security hardening: tie loopback-origin dev allowance to actual local socket clients (not Host header claims), add explicit warnings/metrics when `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback` accepts websocket origins, harden safe-regex detection for quantified ambiguous alternation patterns (for example `(a|aa)+`), and bound large regex-evaluation inputs for session-filter and log-redaction paths.
- Tests/Sandbox + archive portability: use junction-compatible directory-link setup on Windows and explicit file-symlink platform guards in symlink escape tests where unprivileged file symlinks are unavailable, reducing false Windows CI failures while preserving traversal checks on supported paths. (#28747) Thanks @arosstale.
- Tests/Subagent announce: set `OPENCLAW_TEST_FAST=1` before importing `subagent-announce` format suites so module-level fast-mode constants are captured deterministically on Windows CI, preventing timeout flakes in nested completion announce coverage. (#31370) Thanks @zwffff.
- Gateway/Node dangerous-command parity: include `sms.send` in default onboarding node `denyCommands`, share onboarding deny defaults with the gateway dangerous-command source of truth, and include `sms.send` in phone-control `/phone arm writes` handling so SMS follows the same break-glass flow as other dangerous node commands. Thanks @zpbrent.
- Zalo/Pairing auth tests: add webhook regression coverage asserting DM pairing-store reads/writes remain account-scoped, preventing cross-account authorization bleed in multi-account setups. (#26121) Thanks @bmendonca3.

View File

@@ -148,6 +148,10 @@ describe("applyPatch", () => {
});
it("rejects symlink escape attempts by default", async () => {
// File symlinks require SeCreateSymbolicLinkPrivilege on Windows.
if (process.platform === "win32") {
return;
}
await withTempDir(async (dir) => {
const outside = path.join(path.dirname(dir), "outside-target.txt");
const linkPath = path.join(dir, "link.txt");
@@ -232,6 +236,10 @@ describe("applyPatch", () => {
});
it("allows symlinks that resolve within cwd by default", async () => {
// File symlinks require SeCreateSymbolicLinkPrivilege on Windows.
if (process.platform === "win32") {
return;
}
await withTempDir(async (dir) => {
const target = path.join(dir, "target.txt");
const linkPath = path.join(dir, "link.txt");
@@ -259,7 +267,9 @@ describe("applyPatch", () => {
await fs.writeFile(outsideFile, "victim\n", "utf8");
const linkDir = path.join(dir, "linkdir");
await fs.symlink(outsideDir, linkDir);
// Use 'junction' on Windows — junctions target directories without
// requiring SeCreateSymbolicLinkPrivilege.
await fs.symlink(outsideDir, linkDir, process.platform === "win32" ? "junction" : undefined);
const patch = `*** Begin Patch
*** Delete File: linkdir/victim.txt
@@ -310,7 +320,13 @@ describe("applyPatch", () => {
await fs.writeFile(outsideTarget, "keep\n", "utf8");
const linkDir = path.join(dir, "link");
await fs.symlink(outsideDir, linkDir);
// Use 'junction' on Windows — junctions target directories without
// requiring SeCreateSymbolicLinkPrivilege.
await fs.symlink(
outsideDir,
linkDir,
process.platform === "win32" ? "junction" : undefined,
);
const patch = `*** Begin Patch
*** Delete File: link

View File

@@ -308,6 +308,10 @@ describe("sandbox fs bridge shell compatibility", () => {
it("rejects pre-existing host symlink escapes before docker exec", async () => {
await withTempDir("openclaw-fs-bridge-", async (stateDir) => {
const { workspaceDir, outsideFile } = await createHostEscapeFixture(stateDir);
// File symlinks require SeCreateSymbolicLinkPrivilege on Windows.
if (process.platform === "win32") {
return;
}
await fs.symlink(outsideFile, path.join(workspaceDir, "link.txt"));
const bridge = createSandboxFsBridge({

View File

@@ -103,17 +103,22 @@ describe("validateBindMounts", () => {
});
it("blocks symlink escapes into blocked directories", () => {
const dir = mkdtempSync(join(tmpdir(), "openclaw-sbx-"));
const link = join(dir, "etc-link");
symlinkSync("/etc", link);
const run = () => validateBindMounts([`${link}/passwd:/mnt/passwd:ro`]);
if (process.platform === "win32") {
// Windows source paths (e.g. C:\...) are intentionally rejected as non-POSIX.
// Symlinks to non-existent targets like /etc require
// SeCreateSymbolicLinkPrivilege on Windows. The Windows branch of this
// test does not need a real symlink — it only asserts that Windows source
// paths are rejected as non-POSIX.
const dir = mkdtempSync(join(tmpdir(), "openclaw-sbx-"));
const fakePath = join(dir, "etc-link", "passwd");
const run = () => validateBindMounts([`${fakePath}:/mnt/passwd:ro`]);
expect(run).toThrow(/non-absolute source path/);
return;
}
const dir = mkdtempSync(join(tmpdir(), "openclaw-sbx-"));
const link = join(dir, "etc-link");
symlinkSync("/etc", link);
const run = () => validateBindMounts([`${link}/passwd:/mnt/passwd:ro`]);
expect(run).toThrow(/blocked path/);
});

View File

@@ -120,7 +120,13 @@ describe("archive utils", () => {
await withArchiveCase("zip", async ({ workDir, archivePath, extractDir }) => {
const outsideDir = path.join(workDir, "outside");
await fs.mkdir(outsideDir, { recursive: true });
await fs.symlink(outsideDir, path.join(extractDir, "escape"));
// Use 'junction' on Windows — junctions target directories without
// requiring SeCreateSymbolicLinkPrivilege.
await fs.symlink(
outsideDir,
path.join(extractDir, "escape"),
process.platform === "win32" ? "junction" : undefined,
);
const zip = new JSZip();
zip.file("escape/pwn.txt", "owned");