fix: allow trusted exec approvals home symlinks (#72377)

This commit is contained in:
Vincent Koc
2026-04-26 14:57:01 -07:00
committed by GitHub
parent baaad52389
commit 364d49889e
3 changed files with 24 additions and 8 deletions

View File

@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Exec approvals: accept a symlinked `OPENCLAW_HOME` as the trusted approvals root while still rejecting symlinked `.openclaw` path components below it. (#64663) Thanks @FunJim.
- 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.
- 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.
- 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.

View File

@@ -187,17 +187,34 @@ describe("exec approvals store helpers", () => {
expect(fs.readFileSync(targetPath, "utf8")).toBe('{"sentinel":true}\n');
});
it("refuses to traverse a symlinked parent component in the approvals path", () => {
it("accepts a symlinked OPENCLAW_HOME as the trusted approvals root", () => {
const realHome = makeTempDir();
const linkedHome = `${realHome}-link`;
tempDirs.push(realHome);
fs.symlinkSync(realHome, linkedHome);
tempDirs.push(realHome, linkedHome);
fs.symlinkSync(realHome, linkedHome, "dir");
process.env.OPENCLAW_HOME = linkedHome;
saveExecApprovals({ version: 1, defaults: { security: "full" }, agents: {} });
expect(
fs.readFileSync(path.join(realHome, ".openclaw", "exec-approvals.json"), "utf8"),
).toContain('"security": "full"');
});
it("refuses to traverse symlinked approvals components below a symlinked home", () => {
const realHome = makeTempDir();
const linkedHome = `${realHome}-link`;
const linkedStateTarget = path.join(realHome, "state-target");
tempDirs.push(realHome, linkedHome);
fs.mkdirSync(linkedStateTarget, { recursive: true });
fs.symlinkSync(realHome, linkedHome, "dir");
fs.symlinkSync(linkedStateTarget, path.join(realHome, ".openclaw"), "dir");
process.env.OPENCLAW_HOME = linkedHome;
expect(() =>
saveExecApprovals({ version: 1, defaults: { security: "full" }, agents: {} }),
).toThrow(/Refusing to traverse symlink in exec approvals path/);
expect(fs.existsSync(path.join(realHome, ".openclaw"))).toBe(false);
expect(fs.existsSync(path.join(linkedStateTarget, "exec-approvals.json"))).toBe(false);
});
it("adds trimmed allowlist entries once and persists generated ids", () => {

View File

@@ -248,10 +248,8 @@ function assertNoSymlinkPathComponents(targetPath: string, trustedRoot: string):
const relative = path.relative(resolvedRoot, resolvedTarget);
const segments = relative && relative !== "." ? relative.split(path.sep) : [];
let current = resolvedRoot;
for (const segment of [".", ...segments]) {
if (segment !== ".") {
current = path.join(current, segment);
}
for (const segment of segments) {
current = path.join(current, segment);
try {
const stat = fs.lstatSync(current);
if (stat.isSymbolicLink()) {