Skip to content

Commit 4461508

Browse files
committed
Limit path traversal to skill dir
1 parent 44b1343 commit 4461508

2 files changed

Lines changed: 13 additions & 1 deletion

File tree

structure/links.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@ func CheckInternalLinks(dir, body string) []types.Result {
4040
continue
4141
}
4242
// Relative link — check file existence
43-
resolved := filepath.Join(dir, link)
43+
resolved := filepath.Clean(filepath.Join(dir, link))
44+
// Block path traversal: the resolved path must stay inside the skill directory.
45+
if !strings.HasPrefix(resolved, filepath.Clean(dir)+string(filepath.Separator)) {
46+
results = append(results, ctx.Errorf("internal link escapes skill directory: %s", link))
47+
continue
48+
}
4449
if _, err := os.Stat(resolved); os.IsNotExist(err) {
4550
results = append(results, ctx.Errorf("broken internal link: %s (file not found)", link))
4651
} else {

structure/links_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ func TestCheckInternalLinks(t *testing.T) {
5757
requireResult(t, results, types.Pass, "internal link: references/guide.md (exists)")
5858
})
5959

60+
t.Run("path traversal is blocked", func(t *testing.T) {
61+
dir := t.TempDir()
62+
body := "See [escape](../../../../../../etc/passwd)."
63+
results := CheckInternalLinks(dir, body)
64+
requireResult(t, results, types.Error, "internal link escapes skill directory: ../../../../../../etc/passwd")
65+
})
66+
6067
t.Run("no links returns nil", func(t *testing.T) {
6168
dir := t.TempDir()
6269
body := "No links here."

0 commit comments

Comments
 (0)