Skip to content

Enforce symlink containment for reads and internal links - #90

Merged
dacharyc merged 1 commit into
mainfrom
fix/symlink-containment
Aug 4, 2026
Merged

Enforce symlink containment for reads and internal links#90
dacharyc merged 1 commit into
mainfrom
fix/symlink-containment

Conversation

@dacharyc

@dacharyc dacharyc commented Aug 4, 2026

Copy link
Copy Markdown
Member

What this PR does

Fixes #86 and #88 with a shared mechanism: resolution-based containment. The skill root and each candidate path are resolved with filepath.EvalSymlinks, and the resolved path must remain inside the resolved root.

  • SafeReadFile does not guard against symlinked directories #88: util.SafeReadFile now takes the skill root and adds a resolved-path containment check on top of the existing leaf Lstat check. A symlinked directory (e.g. references/ pointing outside the skill tree) no longer exposes out-of-tree regular files to reads, so that content cannot be shipped to the LLM judge. The signature change is safe: SafeReadFile was introduced after v1.5.5 and has not appeared in any release.
  • Should internal-link validation reject symlinks that resolve outside the skill directory? #86: CheckInternalLinks previously verified containment syntactically and then called os.Stat, which follows symlinks. It now resolves the target and reports internal link escapes skill directory: <link> (resolves outside the skill package) as an error. Symlinks that resolve within the package still pass link validation.

File symlinks remain unread everywhere (the #78 rule is unchanged); this PR only tightens.

How to test

Verified end-to-end with a hostile fixture (a skill whose references/ is a symlink to an outside directory and whose SKILL.md links to a file within it): the link is reported as escaping and the read is refused. Also:

  • go test -race ./... -count=1
  • New tests: symlinked-directory read refusal and in-tree symlinked-directory acceptance (util), escaping and in-tree symlink link targets (structure), symlinked references/ never reaching judge inputs (skillcheck)

Checklist

  • Tests pass locally (go test -race ./... -count=1)
  • Lint passes locally (golangci-lint run)
  • New functionality includes tests
  • Breaking changes are noted above (if any)

Fixes #86, fixes #88.

SafeReadFile now takes the skill root and, in addition to the existing
leaf Lstat check, verifies that the fully resolved path stays inside the
resolved root. This closes the bypass where a symlinked directory (for
example references/ pointing outside the skill tree) exposed regular
files that passed the leaf-only check and were read and shipped to the
LLM judge. The signature change is safe: SafeReadFile was introduced
after v1.5.5 and has not appeared in a release.

Internal link validation previously checked containment syntactically
and then called os.Stat, which follows symlinks, so a link target inside
the skill directory could resolve outside it. CheckInternalLinks now
resolves the target and reports "internal link escapes skill directory"
when the resolved path leaves the skill package. Symlinks that resolve
within the package still pass link validation.
@dacharyc
dacharyc merged commit 8469932 into main Aug 4, 2026
3 checks passed
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.

Should internal-link validation reject symlinks that resolve outside the skill directory?

1 participant