| id | 84 |
|---|---|
| title | Symlink default-deny for file discovery |
| status | ✅ |
| summary | Skip symlinks by default during directory walks; add --follow-symlinks opt-in flag. |
Prevent symlink-based attacks. A malicious symlink in
a repo can cause mdsmith check to read or
mdsmith fix to overwrite files outside the project.
filepath.Walk in walkDir follows file symlinks by
default. The existing --no-follow-symlinks CLI flag
is opt-in and skips all symlinks by setting the
pattern-based no-follow-symlinks config key to **.
A symlink like
ln -s /etc/cron.d/jobs evil.md in a repo causes
mdsmith fix . to overwrite the target.
Among compared linters, only Prettier v3 rejects symlinks (PR #14627). textlint is incidentally safe (glob default). markdownlint, Vale, and remark-lint all follow symlinks.
Replace NoFollowSymlinks []string (pattern-based
opt-out) with FollowSymlinks bool (global opt-in).
In walkDir, skip all entries where
info.Mode()&os.ModeSymlink != 0 unless
FollowSymlinks is true.
Replace --no-follow-symlinks with
--follow-symlinks:
mdsmith check --follow-symlinks .
mdsmith fix --follow-symlinks .Replace no-follow-symlinks with follow-symlinks:
follow-symlinks: true # opt-in, default false- Deprecate
no-follow-symlinksconfig key: if present, emit a warning suggesting migration to the newfollow-symlinks: falsedefault. - Remove
--no-follow-symlinksCLI flag outright: the polarity flipped (--follow-symlinksis the new opt-in) and keeping a negated sibling is both redundant with the new secure default and confusing next to it. Passing the removed flag errors out on parse (exit 2), same as any other unknown flag.
The write-side TOCTOU is handled by plan 83 section C
(atomic writes). os.Rename(tmp, path) replaces the
symlink
itself, not the target — no separate Lstat check
needed.
Consider migrating from filepath.Walk to
filepath.WalkDir (Go 1.16+). WalkDir provides
d.Type() with fs.ModeSymlink without extra
os.Lstat calls. This makes symlink detection cheaper.
- Replaced
NoFollowSymlinks []stringwithFollowSymlinks boolinconfig.Config; keptLegacyNoFollowSymlinksfor deprecation parsing - Replaced
--no-follow-symlinkswith--follow-symlinks; old flag removed outright - Updated
ResolveOptsto useFollowSymlinks bool - Updated
walkDirto skip symlinks by default - Updated
resolveGlobto skip symlinks by default - Added deprecation warning for old config key
(emitted by
cmd/mdsmith.loadConfigonce per run) - Updated
files_test.goandlint_coverage_test.go - Added integration tests in
cmd/mdsmith/e2e_symlink_default_deny_test.go: external-target symlink skipped by default, followed with--follow-symlinks, config-key opt-in, legacy-config deprecation warning, and fix TOCTOU behavior (symlink replaced, target untouched)
- Symlinks are skipped by default in directory walks
-
--follow-symlinksflag enables symlink following -
follow-symlinks: truein config enables symlink following - Old
no-follow-symlinksconfig emits deprecation warning - Both
checkandfixrespect the setting - All tests pass:
go test ./...(except pre-existinginternal/corpusfailures tracked by plan 90) -
go tool golangci-lint runreports no issues