Skip to content

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

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#60144
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

Bug

The skills.workspace.symlink_escape audit probe checks whether skill file realpaths escape the workspace root (~/.openclaw/workspace/), but the skill loader (resolveContainedSkillPath in src/agents/skills/workspace.ts) checks against the skills directory root (~/.openclaw/workspace/skills/).

This means symlinks like:

workspace/skills/my-skill -> ../ehoy/.claude/skills/my-skill

...resolve to workspace/ehoy/.claude/skills/my-skill, which is:

  • ✅ INSIDE the workspace root → audit says OK
  • ❌ OUTSIDE workspace/skills/ → loader silently rejects it

openclaw security audit reports no findings, but the skill is silently dropped at load time with zero user feedback.

How We Found It

We had 10 symlinked skills that were silently invisible for nearly a month after v2026.3.7 shipped the realpath enforcement in the loader. The daily-review cron skill stopped working but the model was finding and executing the skill through manual filesystem search ~80% of the time, masking the fact that formal skill discovery never included it. openclaw security audit showed nothing wrong.

Fix

Changed the audit probe to check against the skills directory realpath (workspace/skills/) instead of the workspace root, matching the loader's boundary. Updated finding messages to reflect the tighter boundary.

Test

Added a test case for the specific gap: a symlink that resolves inside the workspace root but outside the skills directory now correctly triggers the skills.workspace.symlink_escape finding.

All existing tests pass. The existing "warns when workspace skill files resolve outside workspace root" test continues to pass (those escapes are a superset of the new check).

AI-Assisted

This PR was authored by an AI agent (Claude Opus via OpenClaw). The code changes were verified against the existing test suite and a new test case was added. The agent reviewed git blame and commit history to confirm this was an oversight (the probe was added Mar 2 before the Mar 8 loader hardening), not intentional behavior.

Relates to #49408

@greptile-apps

greptile-apps Bot commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a gap between the skills.workspace.symlink_escape audit probe and the skill loader: the probe was checking symlink realpaths against the workspace root (~/.openclaw/workspace/), while the loader (resolveContainedSkillPath) checks against the skills directory root (~/.openclaw/workspace/skills/). Symlinks resolving inside the workspace but outside skills/ were silently dropped at load time with no audit finding. The fix tightens the probe boundary to match the loader, adds a targeted regression test for the specific gap, and updates the finding messages accordingly.

Confidence Score: 5/5

Safe to merge — targeted, correct fix with a new test case covering the exact gap.

The change is minimal and mechanically correct: the audit probe now uses realpath(workspace/skills/) as the containment boundary, which matches exactly what the loader (resolveContainedSkillPath) uses via baseDirRealPath. The fallback in fs.realpath(skillsDir).catch(() => skillsDir) is consistent with the pre-existing pattern. The new test verifies the specific gap (inside workspace, outside skills/), and the existing warns outside workspace root test remains a valid superset check. No regressions expected.

No files require special attention.

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

The skills.workspace.symlink_escape audit probe checked whether skill
file realpaths escaped the workspace root (~/.openclaw/workspace/),
but the skill loader (resolveContainedSkillPath) checks against the
skills directory root (~/.openclaw/workspace/skills/).

This mismatch meant symlinks like:
  workspace/skills/my-skill -> workspace/other-dir/skills/my-skill

...would resolve inside the workspace root (audit says OK) but outside
the skills directory (loader silently rejects). The skill would fail
to load with zero feedback from `openclaw security audit`.

Fix: check against the skills directory realpath in the audit probe,
matching the loader's boundary. Add test for the in-workspace but
outside-skills-dir case.

Relates to openclaw#49408
@menhguin menhguin closed this Apr 3, 2026
@menhguin
menhguin force-pushed the fix/audit-skill-symlink-escape-boundary branch from d561e96 to b40c56a Compare April 3, 2026 08:14
@menhguin
menhguin requested review from a team and steipete as code owners April 3, 2026 08:14
@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation channel: bluebubbles Channel integration: bluebubbles channel: discord Channel integration: discord channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: line Channel integration: line channel: matrix Channel integration: matrix channel: mattermost Channel integration: mattermost channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: nostr Channel integration: nostr channel: signal Channel integration: signal channel: slack Channel integration: slack channel: telegram Channel integration: telegram channel: tlon Channel integration: tlon channel: whatsapp-web Channel integration: whatsapp-web channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser app: android App: android app: ios App: ios app: macos App: macos labels Apr 3, 2026
@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

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

Labels

app: android App: android app: ios App: ios app: macos App: macos channel: bluebubbles Channel integration: bluebubbles channel: discord Channel integration: discord channel: feishu Channel integration: feishu channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: irc channel: line Channel integration: line channel: matrix Channel integration: matrix channel: mattermost Channel integration: mattermost channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: nostr Channel integration: nostr channel: qqbot channel: signal Channel integration: signal channel: slack Channel integration: slack channel: telegram Channel integration: telegram channel: tlon Channel integration: tlon channel: twitch Channel integration: twitch channel: whatsapp-web Channel integration: whatsapp-web channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser docker Docker and sandbox tooling docs Improvements or additions to documentation extensions: acpx extensions: anthropic extensions: byteplus extensions: cloudflare-ai-gateway extensions: copilot-proxy Extension: copilot-proxy extensions: deepseek extensions: device-pair extensions: diagnostics-otel Extension: diagnostics-otel extensions: duckduckgo extensions: fal extensions: huggingface extensions: kilocode extensions: kimi-coding extensions: llm-task Extension: llm-task extensions: lobster Extension: lobster gateway Gateway runtime security Security documentation size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant