Skip to content

security: confine skills-sync destinations to the cloned repo root - #116

Merged
bketelsen merged 3 commits into
mainfrom
cockpit/worker-74cd53a0cc74702a
Aug 24, 2026
Merged

security: confine skills-sync destinations to the cloned repo root#116
bketelsen merged 3 commits into
mainfrom
cockpit/worker-74cd53a0cc74702a

Conversation

@bketelsen

Copy link
Copy Markdown
Contributor

Summary

scripts/sync-skills.sh resolved a freshly cloned consumer repo's .agents/skills root and each managed-skill destination with realpath -m but never checked the result stayed inside the temporary clone. A consumer repo (untrusted input to the sync) that planted a .agents symlink — or a pre-existing .agents/skills/<skill> symlink — pointing outside the clone could turn the sync's mkdir, rsync --delete, or marker write into an escape from the clone root.

  • Add skills_sync_require_contained / skills_sync_sync_repo in scripts/lib/skills-sync-containment.sh, shared by sync-skills.sh and its regression tests. skills_sync_sync_repo fails closed — before any mkdir, rsync --delete, or marker write — when the resolved .agents/skills root or a skill's destination is not strictly contained in the clone root.
  • sync-skills.sh now sources the library and delegates the per-skill mkdir/rsync/marker work to skills_sync_sync_repo, aborting (and marking fail=1) that repo's sync on a containment violation instead of mutating outside the clone.
  • Supported in-repo layouts such as .agents -> docs/agents still resolve and sync normally, since the resolved path stays under the clone root.

Test plan

  • test/sync-skills-containment.test.mjs (new): proves an escaping .agents symlink and an escaping existing managed-skill destination symlink are rejected without creating/deleting anything under an outside sentinel directory, while a normal .agents directory and an in-repo .agents -> docs/agents symlink still sync and write the marker.
  • npm run check (root) passes — check:docs, check:organization, and npm test (15 tests, including the 4 new containment tests) all green.

🤖 Generated with Claude Code

bketelsen and others added 3 commits August 24, 2026 20:21
sync-skills.sh resolved a consumer repo's .agents symlink and each
managed-skill destination with realpath -m but never checked the result
stayed inside the temporary clone. An untrusted consumer repo could
plant a .agents symlink (or a pre-existing .agents/skills/<skill>
symlink) pointing outside the clone and turn the sync's mkdir,
rsync --delete, or marker write into an escape from the clone root.

Add skills_sync_require_contained/skills_sync_sync_repo in
scripts/lib/skills-sync-containment.sh, shared by sync-skills.sh and
its regression tests, which fails closed before any mutation when the
resolved skills root or a skill's destination is not strictly
contained in the clone. In-repo layouts such as .agents -> docs/agents
still resolve and sync normally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Precompute and validate every selected managed-skill destination (and
the skills root) in a validation-only pass before entering the
mkdir/rsync/marker-write loop, so an escaping destination later in the
skill list can no longer be caught after an earlier valid destination
was already mutated.

Addresses review blocker defect:scripts/lib/skills-sync-containment.sh:validate-all-before-mutation
skills_sync_require_contained accepted path_real == root_real, so an
in-clone `.agents/skills -> ..` symlink could resolve exactly to the
clone root and slip past the containment check, letting sync write
skill contents and markers directly into the clone root instead of
the managed .agents/skills subtree.

Require strict containment (root_real/* only) for both the
.agents/skills root and each skill destination, and add a regression
covering the root-equality symlink case.
@bketelsen
bketelsen marked this pull request as ready for review August 24, 2026 23:05
@bketelsen
bketelsen added this pull request to the merge queue Aug 24, 2026
Merged via the queue into main with commit aa5d11b Aug 24, 2026
2 checks passed
@bketelsen
bketelsen deleted the cockpit/worker-74cd53a0cc74702a branch August 24, 2026 23:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant