|
1 | 1 | import { describe, expect, test } from "bun:test"; |
2 | | -import { resolve } from "node:path"; |
| 2 | +import { mkdtempSync, realpathSync, rmSync, symlinkSync } from "node:fs"; |
| 3 | +import { tmpdir } from "node:os"; |
| 4 | +import { join, resolve } from "node:path"; |
3 | 5 | import { |
4 | 6 | matchesWritePathAllowlist, |
5 | 7 | writePathDeniedReason, |
@@ -58,6 +60,39 @@ describe("matchesWritePathAllowlist", () => { |
58 | 60 | }); |
59 | 61 | }); |
60 | 62 |
|
| 63 | +describe("matchesWritePathAllowlist with a symlinked cwd", () => { |
| 64 | + test("allows a write under the canonical target of a symlinked cwd", () => { |
| 65 | + // Mirrors macOS's /tmp -> /private/tmp: cwd is spelled via the symlink, |
| 66 | + // but resolveWorkspacePath (and any tool arg it rewrites) hands the |
| 67 | + // subject in already realpathed. Both sides of the compare must |
| 68 | + // canonicalize the same way or a legitimate write is hard-denied. |
| 69 | + const real = mkdtempSync(join(realpathSync(tmpdir()), "write-path-real-")); |
| 70 | + const linkDir = join(realpathSync(tmpdir()), `write-path-link-${process.pid}`); |
| 71 | + try { |
| 72 | + symlinkSync(real, linkDir); |
| 73 | + const symlinkedCwd = linkDir; // lexically distinct from `real` |
| 74 | + const canonicalSubject = join(real, "docs", "a.md"); // already realpathed |
| 75 | + |
| 76 | + expect( |
| 77 | + matchesWritePathAllowlist(canonicalSubject, ["docs/*"], symlinkedCwd), |
| 78 | + ).toBe(true); |
| 79 | + |
| 80 | + // A genuinely outside path is still denied. |
| 81 | + const outsideReal = mkdtempSync(join(realpathSync(tmpdir()), "write-path-outside-")); |
| 82 | + try { |
| 83 | + expect( |
| 84 | + matchesWritePathAllowlist(join(outsideReal, "docs", "a.md"), ["docs/*"], symlinkedCwd), |
| 85 | + ).toBe(false); |
| 86 | + } finally { |
| 87 | + rmSync(outsideReal, { recursive: true, force: true }); |
| 88 | + } |
| 89 | + } finally { |
| 90 | + rmSync(linkDir, { force: true }); |
| 91 | + rmSync(real, { recursive: true, force: true }); |
| 92 | + } |
| 93 | + }); |
| 94 | +}); |
| 95 | + |
61 | 96 | describe("writePathDeniedReason", () => { |
62 | 97 | test("names allowlist and subject", () => { |
63 | 98 | const reason = writePathDeniedReason("src/x.ts", ["PRODUCT.md", "docs/*"]); |
|
0 commit comments