Skip to content

fix(security): align audit symlink_escape boundary with skill loader#60155

Closed
menhguin wants to merge 1 commit into
openclaw:mainfrom
menhguin:fix/audit-skill-symlink-escape-boundary
Closed

fix(security): align audit symlink_escape boundary with skill loader#60155
menhguin wants to merge 1 commit into
openclaw:mainfrom
menhguin:fix/audit-skill-symlink-escape-boundary

Conversation

@menhguin

@menhguin menhguin commented Apr 3, 2026

Copy link
Copy Markdown

Problem

The skills.workspace.symlink_escape audit probe checks whether workspace skill realpaths escape the workspace root, but the skill loader (resolveContainedSkillPath) checks whether they escape the skills directory root (workspace/skills/). This creates a gap where symlinks that resolve inside the workspace but outside the skills directory pass the audit but are silently rejected at load time.

Example: workspace/skills/my-skill -> ../shared/skills/my-skill resolves to workspace/shared/skills/my-skill — inside the workspace root (audit passes ✅) but outside workspace/skills/ (loader rejects ❌).

Impact

We had 10 symlinked skills silently invisible for nearly a month. The daily-review cron skill stopped working reliably when v2026.3.7 shipped the loader enforcement — but openclaw security audit never flagged it. The model compensated by discovering the skill file through manual filesystem search ~80% of the time, masking the issue.

Fix

Align the audit probe boundary with the loader: check against the skills directory root, not the workspace root. One-line logic change + updated finding messages + test for the gap case.

Changes

  • audit-extra.async.ts: resolve skillsDir (workspace/skills) instead of workspacePath, use it as the containment boundary
  • audit.test.ts: add test case for symlink inside workspace but outside skills dir
  • Updated finding title/detail/remediation to reference "skills directory" instead of "workspace root"
  • Added skills.load.extraDirs mention in remediation text

CI Note

Windows test failures in task-owner-access.test.ts are pre-existing timeouts, unrelated to this change.

Closes #60145

The audit probe checked against the workspace root, but the skill loader
(resolveContainedSkillPath) checks against the skills directory root.
Symlinks resolving inside the workspace but outside workspace/skills/
passed audit but were silently rejected at load time.

Align the audit boundary with the loader so these cases are flagged.
@menhguin
menhguin requested a review from a team as a code owner April 3, 2026 08:18
@greptile-apps

greptile-apps Bot commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR tightens the skills.workspace.symlink_escape audit probe to use workspace/skills/ as the containment boundary instead of the workspace root, aligning it with how resolveContainedSkillPath in the skill loader actually enforces containment. The logic change is a one-liner, the updated finding messages are accurate, and the new test case directly covers the previously-invisible gap (symlink resolving inside workspace but outside the skills dir).

Confidence Score: 5/5

Safe to merge — the boundary fix is correct and well-tested; only a minor test comment mismatch remains.

All findings are P2 (a misleading comment in the new test). The core logic change correctly aligns the audit boundary with the loader, and the new test validates the previously-missed gap scenario.

No files require special attention.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/security/audit.test.ts
Line: 1397-1401

Comment:
**Test comment describes relative symlink, code creates absolute**

The comment says `../../shared/skills/my-skill/SKILL.md` (relative), but `fs.symlink` receives an absolute path via `path.join(sharedSkillDir, "SKILL.md")`. Both types correctly exercise the escape scenario here, but the comment mismatches the actual symlink type and could mislead someone copying this test pattern.

```suggestion
          // Symlink: skills/linked-skill/SKILL.md -> <abs>/workspace/shared/skills/my-skill/SKILL.md
          await fs.symlink(
            path.join(sharedSkillDir, "SKILL.md"),
            path.join(workspaceDir, "skills", "linked-skill", "SKILL.md"),
          );
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(security): align audit symlink_escap..." | Re-trigger Greptile

Comment on lines +1397 to +1401
// Symlink: skills/linked-skill/SKILL.md -> ../../shared/skills/my-skill/SKILL.md
await fs.symlink(
path.join(sharedSkillDir, "SKILL.md"),
path.join(workspaceDir, "skills", "linked-skill", "SKILL.md"),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Test comment describes relative symlink, code creates absolute

The comment says ../../shared/skills/my-skill/SKILL.md (relative), but fs.symlink receives an absolute path via path.join(sharedSkillDir, "SKILL.md"). Both types correctly exercise the escape scenario here, but the comment mismatches the actual symlink type and could mislead someone copying this test pattern.

Suggested change
// Symlink: skills/linked-skill/SKILL.md -> ../../shared/skills/my-skill/SKILL.md
await fs.symlink(
path.join(sharedSkillDir, "SKILL.md"),
path.join(workspaceDir, "skills", "linked-skill", "SKILL.md"),
);
// Symlink: skills/linked-skill/SKILL.md -> <abs>/workspace/shared/skills/my-skill/SKILL.md
await fs.symlink(
path.join(sharedSkillDir, "SKILL.md"),
path.join(workspaceDir, "skills", "linked-skill", "SKILL.md"),
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/security/audit.test.ts
Line: 1397-1401

Comment:
**Test comment describes relative symlink, code creates absolute**

The comment says `../../shared/skills/my-skill/SKILL.md` (relative), but `fs.symlink` receives an absolute path via `path.join(sharedSkillDir, "SKILL.md")`. Both types correctly exercise the escape scenario here, but the comment mismatches the actual symlink type and could mislead someone copying this test pattern.

```suggestion
          // Symlink: skills/linked-skill/SKILL.md -> <abs>/workspace/shared/skills/my-skill/SKILL.md
          await fs.symlink(
            path.join(sharedSkillDir, "SKILL.md"),
            path.join(workspaceDir, "skills", "linked-skill", "SKILL.md"),
          );
```

How can I resolve this? If you propose a fix, please make it concise.

@clawsweeper

clawsweeper Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Closing this as duplicate or superseded after Codex automated review.

Close #60155 as superseded by the later open PR #60513, which tracks the same remaining security-audit boundary fix. Current main has not implemented the fix yet, so this is not an implemented-on-main close.

Best possible solution:

Close #60155 as duplicate/superseded by open #60513, then use #60513 or a maintainer branch to port the boundary fix onto the current audit-workspace-skills.ts implementation, update the dedicated audit-workspace-skill-escape test, and align the audit-check docs/finding copy with the skills-directory boundary.

What I checked:

So I’m closing this here and keeping the remaining discussion on the canonical linked item.

Codex Review notes: model gpt-5.5, reasoning high; reviewed against e29d3516bf05.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: security audit symlink_escape probe uses wrong boundary (workspace root vs skills directory)

1 participant