fix: allow trusted exec approvals home symlinks#72377
Conversation
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
1. 🟡 Symlink root accepted for OPENCLAW_HOME allows redirecting exec-approvals.json writes to arbitrary location
Description
However, after the change, Key impact:
Vulnerable logic (root not checked): let current = resolvedRoot;
for (const segment of segments) {
current = path.join(current, segment);
const stat = fs.lstatSync(current);
if (stat.isSymbolicLink()) {
throw new Error(`Refusing to traverse symlink in exec approvals path: ${current}`);
}
}While rejecting symlinks below the root still helps, accepting a symlinked root removes an important safety property for environments where RecommendationIf Options:
// Before iterating segments
const rootStat = fs.lstatSync(resolvedRoot);
if (rootStat.isSymbolicLink()) {
throw new Error(`Refusing symlinked OPENCLAW_HOME approvals root: ${resolvedRoot}`);
}
const resolvedRoot = fs.realpathSync(path.resolve(trustedRoot));
const resolvedTarget = path.resolve(targetPath);
// ensure resolvedTarget is within resolvedRoot, then lstat each component
Also document clearly that 2. 🟡 TOCTOU symlink race in exec approvals directory validation can lead to arbitrary file write
DescriptionThe exec approvals writer attempts to prevent symlink traversal by With the change, Impact:
Vulnerable sequence: assertNoSymlinkPathComponents(dir, resolveRequiredHomeDir());
fs.mkdirSync(dir, { recursive: true });
...
fs.writeFileSync(tempPath, raw, { mode: 0o600, flag: "wx" });
fs.renameSync(tempPath, filePath);RecommendationAvoid TOCTOU symlink races by performing operations relative to a trusted directory handle and refusing symlinks at use time. In Node.js, consider:
Example hardening (partial): const rootReal = fs.realpathSync.native(resolveRequiredHomeDir());
const approvalsDir = path.join(rootReal, ".openclaw");
fs.mkdirSync(approvalsDir, { mode: 0o700 });
const st = fs.lstatSync(approvalsDir);
if (!st.isDirectory() || st.isSymbolicLink()) throw new Error("unsafe");
const tmpFd = fs.openSync(path.join(approvalsDir, `...tmp`), "wx", 0o600);
try {
fs.writeFileSync(tmpFd, raw);
} finally {
fs.closeSync(tmpFd);
}If the threat model includes execution with elevated privileges, additionally ignore/clear Analyzed PR: #72377 at commit Last updated on: 2026-04-26T21:54:27Z |
Greptile SummaryThis PR fixes a regression in Confidence Score: 5/5Safe to merge — minimal targeted fix with correct behaviour verified by new tests. The logic change is small and well-reasoned: removing the sentinel '.' element skips only the root check, leaving all sub-path symlink checks intact. The existing ensureDir and assertSafeExecApprovalsDestination guards provide defence-in-depth. Tests cover both the acceptance and rejection paths. No security issues introduced. No files require special attention. Reviews (1): Last reviewed commit: "fix: allow trusted exec approvals home s..." | Re-trigger Greptile |
|
Thanks again for landing this. I dug into the merged behavior on current
In that setup, assertNoSymlinkPathComponents(dir, resolveRequiredHomeDir());and const stat = fs.lstatSync(current);
if (stat.isSymbolicLink()) {
throw new Error(`Refusing to traverse symlink in exec approvals path: ${current}`);
}So with: ~/.openclaw -> ~/workspace/openclaw-config/openclaw/.openclaw
I filed the remaining regression separately here so it doesn’t get lost: In short: |
The exec-approvals symlink hardening from openclaw#72377 still rejected a symlinked ~/.openclaw immediate child of the trusted home directory, breaking exec-policy commands for users who manage OpenClaw config via GNU Stow, chezmoi, or other dotfile managers. openclaw#72377 only relaxed the restriction on the OPENCLAW_HOME root itself. Allow exactly one trusted symlink hop at the immediate child of the trusted home root, but only when the realpath target is owned by the current effective user AND is not group/other writable. Deeper symlinks inside the resolved .openclaw tree, additional symlink hops, and unsafe-permission targets remain rejected with the original error message. Updates the existing 'refuses to traverse symlinked approvals components below a symlinked home' test to construct an unsafe target (group-writable) so the deeper-symlink rejection intent of openclaw#72377 is still exercised, and adds a new test for the safe-symlink case from this issue. Fixes openclaw#72572 Refines openclaw#72377, openclaw#64663, openclaw#64050
Summary
Validation
Refs: #64663, #68417, #65736, #62917, #64050.
ProjectClownfish replacement details:
Found 0 warnings and 1 error.