Skip to content

Commit 364d498

Browse files
authored
fix: allow trusted exec approvals home symlinks (#72377)
1 parent baaad52 commit 364d498

3 files changed

Lines changed: 24 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
66

77
### Fixes
88

9+
- Exec approvals: accept a symlinked `OPENCLAW_HOME` as the trusted approvals root while still rejecting symlinked `.openclaw` path components below it. (#64663) Thanks @FunJim.
910
- ACP: route server logs to stderr before Gateway config/bootstrap work so ACP stdout remains JSON-RPC only for IDE integrations. Fixes #49060. Thanks @Hollychou924.
1011
- Logging: propagate internal request trace scopes through Gateway HTTP requests and WebSocket frames so file logs, diagnostic events, agent run traces, model-call traces, OTEL spans, and trusted provider `traceparent` headers share a correlatable `traceId` without logging raw request or model content. Fixes #40353. Thanks @liangruochong44-ui.
1112
- Diagnostics/OTEL: capture privacy-safe model-call request payload bytes, streamed response bytes, first-response latency, and total duration in diagnostic events, plugin hooks, stability snapshots, and OTEL model-call spans/metrics without logging raw model content. Fixes #33832. Thanks @wwh830.

src/infra/exec-approvals-store.test.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,17 +187,34 @@ describe("exec approvals store helpers", () => {
187187
expect(fs.readFileSync(targetPath, "utf8")).toBe('{"sentinel":true}\n');
188188
});
189189

190-
it("refuses to traverse a symlinked parent component in the approvals path", () => {
190+
it("accepts a symlinked OPENCLAW_HOME as the trusted approvals root", () => {
191191
const realHome = makeTempDir();
192192
const linkedHome = `${realHome}-link`;
193-
tempDirs.push(realHome);
194-
fs.symlinkSync(realHome, linkedHome);
193+
tempDirs.push(realHome, linkedHome);
194+
fs.symlinkSync(realHome, linkedHome, "dir");
195+
process.env.OPENCLAW_HOME = linkedHome;
196+
197+
saveExecApprovals({ version: 1, defaults: { security: "full" }, agents: {} });
198+
199+
expect(
200+
fs.readFileSync(path.join(realHome, ".openclaw", "exec-approvals.json"), "utf8"),
201+
).toContain('"security": "full"');
202+
});
203+
204+
it("refuses to traverse symlinked approvals components below a symlinked home", () => {
205+
const realHome = makeTempDir();
206+
const linkedHome = `${realHome}-link`;
207+
const linkedStateTarget = path.join(realHome, "state-target");
208+
tempDirs.push(realHome, linkedHome);
209+
fs.mkdirSync(linkedStateTarget, { recursive: true });
210+
fs.symlinkSync(realHome, linkedHome, "dir");
211+
fs.symlinkSync(linkedStateTarget, path.join(realHome, ".openclaw"), "dir");
195212
process.env.OPENCLAW_HOME = linkedHome;
196213

197214
expect(() =>
198215
saveExecApprovals({ version: 1, defaults: { security: "full" }, agents: {} }),
199216
).toThrow(/Refusing to traverse symlink in exec approvals path/);
200-
expect(fs.existsSync(path.join(realHome, ".openclaw"))).toBe(false);
217+
expect(fs.existsSync(path.join(linkedStateTarget, "exec-approvals.json"))).toBe(false);
201218
});
202219

203220
it("adds trimmed allowlist entries once and persists generated ids", () => {

src/infra/exec-approvals.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,10 +248,8 @@ function assertNoSymlinkPathComponents(targetPath: string, trustedRoot: string):
248248
const relative = path.relative(resolvedRoot, resolvedTarget);
249249
const segments = relative && relative !== "." ? relative.split(path.sep) : [];
250250
let current = resolvedRoot;
251-
for (const segment of [".", ...segments]) {
252-
if (segment !== ".") {
253-
current = path.join(current, segment);
254-
}
251+
for (const segment of segments) {
252+
current = path.join(current, segment);
255253
try {
256254
const stat = fs.lstatSync(current);
257255
if (stat.isSymbolicLink()) {

0 commit comments

Comments
 (0)