Skip to content

Commit aed94cd

Browse files
committed
fix(init): confine pack writes to .mdsmith/ at the write boundary
Enforce the pack contract where pack paths are consumed: validatePackPath rejects any pack.File whose path is absolute or escapes .mdsmith/ with "..", and writeScaffolds refuses a symlinked .mdsmith directory up front. Together these stop a buggy or hostile pack from diverting init's MkdirAll/WriteFile outside the workspace. Covered by TestValidatePackPath, TestWriteScaffolds_RejectsEscapingPath, and TestWriteScaffolds_RefusesSymlinkedMdsmithDir. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fET55utKej7P3VWhiNnLW
1 parent 4d2a410 commit aed94cd

2 files changed

Lines changed: 78 additions & 1 deletion

File tree

cmd/mdsmith/main.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,9 +459,20 @@ func applyPacks(names []string, w io.Writer) error {
459459
// untouched and noted, so a re-run never clobbers a project's edits.
460460
// Existence is checked with statTarget, which refuses a symlinked target
461461
// rather than following it — a planted link must not divert the write
462-
// outside the pack directory. Progress lines go to w.
462+
// outside the pack directory. Each path is validated against the pack
463+
// contract (relative, under .mdsmith/) first, and a symlinked .mdsmith
464+
// directory is refused up front, so a buggy or hostile pack cannot write
465+
// out of tree. Progress lines go to w.
463466
func writeScaffolds(files []pack.File, w io.Writer) error {
467+
// A symlinked .mdsmith directory would let MkdirAll/WriteFile follow it
468+
// and land pack files wherever it points; refuse it before any write.
469+
if info, err := os.Lstat(mdsmithDir); err == nil && info.Mode()&fs.ModeSymlink != 0 {
470+
return fmt.Errorf("%s is a symlink; refusing to write pack files through it", mdsmithDir)
471+
}
464472
for _, f := range files {
473+
if err := validatePackPath(f.Path); err != nil {
474+
return err
475+
}
465476
if dir := filepath.Dir(f.Path); dir != "." {
466477
if err := os.MkdirAll(dir, 0o755); err != nil {
467478
return fmt.Errorf("creating %s: %w", dir, err)
@@ -483,6 +494,29 @@ func writeScaffolds(files []pack.File, w io.Writer) error {
483494
return nil
484495
}
485496

497+
// mdsmithDir is the workspace subdirectory every additive pack writes
498+
// under; validatePackPath keeps pack files confined to it.
499+
const mdsmithDir = ".mdsmith"
500+
501+
// validatePackPath enforces the pack contract at the write boundary: a
502+
// pack file must be a relative path under .mdsmith/. A pack that returns
503+
// an absolute path, or one that escapes the workspace with "..", is
504+
// rejected before any directory is created or file written, so init's
505+
// writes cannot be diverted out of tree. A "../" that stays within
506+
// .mdsmith after cleaning is harmless and allowed; any escape leaves a
507+
// first component other than .mdsmith and is refused.
508+
func validatePackPath(p string) error {
509+
clean := filepath.Clean(p)
510+
if filepath.IsAbs(clean) {
511+
return fmt.Errorf("pack file path %q must be relative", p)
512+
}
513+
first, _, _ := strings.Cut(clean, string(filepath.Separator))
514+
if first != mdsmithDir {
515+
return fmt.Errorf("pack file path %q must be under %s/", p, mdsmithDir)
516+
}
517+
return nil
518+
}
519+
486520
// statTarget reports whether path already exists as a regular file — the
487521
// only kind of entry init treats as "already there, leave it alone". It
488522
// uses Lstat so a symlink is never followed: a symlink, even a dangling

cmd/mdsmith/main_unit_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,6 +1439,49 @@ func TestNormalizePackNames(t *testing.T) {
14391439
assert.Empty(t, normalizePackNames([]string{" ", ""}))
14401440
}
14411441

1442+
func TestValidatePackPath(t *testing.T) {
1443+
// Under .mdsmith/ is the contract; an internal ".." that stays inside
1444+
// is harmless and allowed.
1445+
require.NoError(t, validatePackPath(filepath.Join(".mdsmith", "wordlists", "a.yaml")))
1446+
require.NoError(t, validatePackPath(filepath.Join(".mdsmith", "kinds", "..", "a.yaml")))
1447+
1448+
// Absolute paths and any escape out of .mdsmith/ are refused.
1449+
assert.Error(t, validatePackPath(filepath.FromSlash("/etc/passwd")))
1450+
assert.Error(t, validatePackPath(filepath.Join("..", "evil.yaml")))
1451+
assert.Error(t, validatePackPath(filepath.Join(".mdsmith", "..", "..", "evil.yaml")))
1452+
assert.Error(t, validatePackPath(filepath.Join("other", "a.yaml")))
1453+
}
1454+
1455+
func TestWriteScaffolds_RejectsEscapingPath(t *testing.T) {
1456+
dir := t.TempDir()
1457+
t.Chdir(dir)
1458+
// A pack that returns a path escaping .mdsmith/ is refused before any
1459+
// write, so nothing lands outside the workspace.
1460+
files := []pack.File{{Path: filepath.Join("..", "escape.yaml"), Data: []byte("x")}}
1461+
err := writeScaffolds(files, io.Discard)
1462+
require.Error(t, err)
1463+
assert.Contains(t, err.Error(), "under .mdsmith/")
1464+
_, statErr := os.Stat(filepath.Join(filepath.Dir(dir), "escape.yaml"))
1465+
assert.True(t, os.IsNotExist(statErr), "nothing written outside the workspace")
1466+
}
1467+
1468+
func TestWriteScaffolds_RefusesSymlinkedMdsmithDir(t *testing.T) {
1469+
dir := t.TempDir()
1470+
t.Chdir(dir)
1471+
// .mdsmith itself is a symlink to another directory; pack writes must
1472+
// not be redirected through it.
1473+
target := filepath.Join(dir, "elsewhere")
1474+
require.NoError(t, os.Mkdir(target, 0o755))
1475+
require.NoError(t, os.Symlink(target, ".mdsmith"))
1476+
1477+
files := []pack.File{{Path: filepath.Join(".mdsmith", "wordlists", "a.yaml"), Data: []byte("x")}}
1478+
err := writeScaffolds(files, io.Discard)
1479+
require.Error(t, err)
1480+
assert.Contains(t, err.Error(), "symlink")
1481+
_, statErr := os.Stat(filepath.Join(target, "wordlists", "a.yaml"))
1482+
assert.True(t, os.IsNotExist(statErr), "nothing written through the symlinked .mdsmith")
1483+
}
1484+
14421485
// --- runHelp ---
14431486

14441487
func TestRunHelp_NoArgs_ExitsZero(t *testing.T) {

0 commit comments

Comments
 (0)