| id | 83 |
|---|---|
| title | Security hardening batch |
| status | 🔳 |
| summary | Bundle of low-risk security fixes: ANSI sanitization, path traversal boundary, atomic writes, schema fs.FS, catalog injection warning, CUE timeout, include size limit. |
Ship seven small, low-risk security improvements as a single batch. Each fix is independent but too small to warrant its own plan.
Strip C0/C1 control characters from user-controlled
strings in TextFormatter output to prevent terminal
injection.
Add to internal/output/text.go:
func sanitizeTerminal(s string) string {
return strings.Map(func(r rune) rune {
if r == '\t' || r == '\n' || r == '\r' {
return r
}
if r < 0x20 || r == 0x7f ||
(r >= 0x80 && r <= 0x9f) {
return -1
}
return r
}, s)
}Apply to d.File, d.Message in Format, and
line in writeSourceLine. Do not modify
JSONFormatter (Go's encoding/json already escapes
control chars).
Add project-root boundary check to
cross-file-reference-integrity. Add RootDir string
to lint.File, populate from engine.Runner.RootDir.
In resolveTargetFile, before os.Stat:
rel, err := filepath.Rel(absRoot, absResolved)
if err != nil ||
rel == ".." ||
strings.HasPrefix(
rel,
".."+string(filepath.Separator),
) {
return targetFile{}, false
}Replace os.WriteFile in fix.go:fixFile with
temp-file-then-rename:
tmp, err := os.CreateTemp(filepath.Dir(path), ".mdsmith-fix-*")
if err != nil {
return err
}
tmpPath := tmp.Name()
defer func() {
if tmpPath != "" {
_ = os.Remove(tmpPath)
}
}()
if _, err := tmp.Write(out); err != nil {
tmp.Close()
return err
}
if err := tmp.Close(); err != nil {
return err
}
if err := os.Chmod(tmpPath, info.Mode()); err != nil {
return err
}
if err := os.Rename(tmpPath, path); err != nil {
return err
}
tmpPath = ""Replace os.ReadFile(r.Schema) in
requiredstructure/rule.go:82 with
fs.ReadFile(f.RootFS, r.Schema). Fall back to
f.FS when RootFS is nil.
In the catalog rule's template rendering path, emit
a diagnostic when an interpolated front-matter value
contains embedded newlines or unbalanced ](
sequences. Diagnostic only — no escaping.
The CUE API (CompileString, Unify, Validate) is
not context-aware, so context.WithTimeout cannot
interrupt long-running evaluation. Rely on
GOMEMLIMIT at process level to cap memory. If a
true wall-clock limit is needed in the future, run
CUE evaluation in a separate OS process that can be
killed on timeout.
Replace fs.ReadFile with ReadFSFileLimited at
include/rule.go:194, catalog/rule.go:395,468.
Thread MaxInputBytes via lint.File.
- Add
sanitizeControl/sanitizeSourceLineinoutput/text.go; apply them tod.File,d.Message, and source lines - Add
RootDirtolint.File; populate in runner and fixer - Add
filepath.Relboundary check inresolveTargetFile; silently skip links above root - Replace
os.WriteFilewith temp-file-then-rename infix.go - Replace
os.ReadFile(r.Schema)withfs.ReadFile(f.RootFS, r.Schema); reject absolute and../paths whenRootFSis set - Add newline and
](detection in catalog template rendering; emit diagnostic - Set
GOMEMLIMITat process startup to cap memory for CUE and other unbounded operations - Replace
fs.ReadFilewithReadFSFileLimitedat include and catalog read sites (after plan 81) - Add tests for each fix (A through F)
- ANSI escape bytes (0x1B, 0x9B, 0x07) stripped from text output; header fields remain single-line; source snippets may preserve tabs
- Links traversing above
RootDirare silently skipped; links within root work -
mdsmith fixuses atomic temp-file-then-rename - Schema read via
f.RootFS; absolute/../paths rejected - Catalog values with
\nor](produce a diagnostic -
GOMEMLIMITset at process startup to bound memory for CUE and other operations - Include/catalog reads bounded by
MaxInputBytes(after plan 81) - All tests pass:
go test ./... -
go tool golangci-lint runreports no issues