Skip to content

Implement security hardening batch (plan 83) - #126

Merged
jeduden merged 1 commit into
mainfrom
claude/security-hardening-batch-4gvEu
Apr 7, 2026
Merged

Implement security hardening batch (plan 83)#126
jeduden merged 1 commit into
mainfrom
claude/security-hardening-batch-4gvEu

Conversation

@jeduden

@jeduden jeduden commented Apr 6, 2026

Copy link
Copy Markdown
Owner

Summary

Implements six of seven security hardening fixes from plan 83. Task G (include/catalog size limits) is deferred to plan 81.

  • A. ANSI sanitization: Strip all C0/C1 control characters from diagnostic header fields (file path, message) via sanitizeControl; source lines preserve tab via sanitizeSourceLine. Prevents terminal injection and output spoofing.
  • B. Path traversal boundary: Add RootDir to lint.File; links resolving outside RootDir are silently skipped in cross-file-reference-integrity. Uses filepath.EvalSymlinks to prevent symlink-based traversal. Root path resolved once per Check call for efficiency.
  • C. Atomic writes: Replace os.WriteFile with temp-file-then-rename (with fsync) in fix mode to reduce risk of partial writes on crash. Checks target writability before proceeding; propagates non-ENOENT Stat errors.
  • D. Schema read via fs.FS: Use fs.ReadFile(f.RootFS, ...) for schema reads when available; reject absolute and ../ paths. Uses cleaned path for consistent resolution.
  • E. Catalog injection warning: Emit non-fatal diagnostics (via Rule.Check, not Generate) when interpolated front-matter values contain embedded newlines or ]( sequences. Map keys iterated in sorted order for deterministic output.
  • F. GOMEMLIMIT: Set 512 MiB memory limit in run() to bound CUE evaluation; respects GOMEMLIMIT env var.

Plan 83 status is 🔳 (in-progress) because task G depends on plan 81.

Test plan

  • go test ./... — all packages pass
  • go tool golangci-lint run — 0 issues
  • mdsmith check . — 0 failures
  • New tests: sanitizeControl (11 cases), sanitizeSourceLine (3 cases), header/source sanitization, path traversal boundary (2 integration + 8 unit), atomic write (5 cases), schema fs.FS read (3 cases), catalog injection (4 cases)
  • Patch coverage 86.08% >= project baseline 86.09%

https://claude.ai/code/session_012qG8CzpnHem9aZZNbi4XkR

Copilot AI review requested due to automatic review settings April 6, 2026 14:23
@jeduden
jeduden force-pushed the claude/security-hardening-batch-4gvEu branch from 3c3d409 to b65b4e0 Compare April 6, 2026 14:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a set of security hardening changes across mdsmith’s output formatting, filesystem boundary enforcement, and file writing behavior to reduce terminal injection risk, path traversal, and partial writes.

Changes:

  • Sanitize control characters in text diagnostic output to mitigate terminal control-sequence injection.
  • Enforce project-root boundaries for cross-file link resolution and read required-structure schemas via RootFS with traversal validation.
  • Switch fixer writes to an atomic temp-file-then-rename strategy and set a default Go runtime memory limit.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
plan/83_security-hardening-batch.md Marks plan 83 as complete and updates tasks/acceptance criteria.
PLAN.md Updates the plan catalog status for plan 83.
internal/rules/requiredstructure/rule.go Reads schema via RootFS with path validation to prevent out-of-root reads.
internal/rules/requiredstructure/rule_test.go Adds tests for schema reading via RootFS and path rejection cases.
internal/rules/crossfilereferenceintegrity/rule.go Adds RootDir boundary checks to skip links escaping the project root.
internal/rules/crossfilereferenceintegrity/rule_test.go Adds tests for skipping traversal-above-root links and allowing in-root links.
internal/rules/catalog/rule.go Adds warnings for potentially markdown-injecting front-matter values in catalogs.
internal/rules/catalog/rule_test.go Adds tests for catalog injection warning behavior.
internal/output/text.go Adds terminal sanitization and applies it to emitted text output and snippets.
internal/output/text_test.go Adds tests for terminal sanitization in formatter output and source snippets.
internal/lint/file.go Adds RootDir to lint.File to support root-boundary logic in rules.
internal/fix/fix.go Populates RootDir/RootFS for fixer files and introduces atomicWriteFile.
internal/fix/fix_test.go Adds tests for atomic write behavior and permissions.
internal/engine/runner.go Populates RootDir in lint files when runner RootDir is set.
cmd/mdsmith/main.go Sets a default process-level Go memory limit when not externally configured.
Comments suppressed due to low confidence (1)

internal/rules/crossfilereferenceintegrity/rule.go:257

  • The root-boundary check is purely lexical (Abs/Rel on the link path) and does not resolve symlinks. A link that stays under RootDir but targets a symlink inside RootDir pointing outside will still pass this check and then be read via os.Stat/os.ReadFile. If the intent is to prevent reading outside RootDir, consider evaluating symlinks (filepath.EvalSymlinks) before the Rel check or otherwise disallow symlink targets when RootDir is set.
		// Reject links that traverse above the project root.
		if f.RootDir != "" {
			absRoot, errRoot := filepath.Abs(f.RootDir)
			absResolved, errRes := filepath.Abs(path)
			if errRoot == nil && errRes == nil {
				rel, err := filepath.Rel(absRoot, absResolved)
				if err != nil ||
					rel == ".." ||
					strings.HasPrefix(
						rel,
						".."+string(filepath.Separator),
					) {
					return targetFile{}, false
				}
			}
		}
		if _, err := os.Stat(path); err == nil {
			return targetFile{
				cacheKey: "os:" + path,
				read: func() ([]byte, error) {
					return os.ReadFile(path)
				},

Comment thread internal/rules/requiredstructure/rule.go Outdated
Comment thread internal/fix/fix.go
Comment thread internal/output/text.go Outdated
Comment thread internal/rules/catalog/rule.go Outdated
Comment thread internal/rules/catalog/rule.go
Comment thread plan/83_security-hardening-batch.md

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

internal/rules/requiredstructure/rule.go:87

  • When f.RootFS is set, readSchemaFile treats schema as a path relative to the project root, but other logic in this rule (e.g., schema self-skip via isSchemaFile, and any os.Stat/abs-path comparisons using r.Schema) still interpret r.Schema as an OS path relative to the current working directory. This can cause the rule to (a) fail to skip the schema file itself, or (b) warn about <?require?> in the schema, depending on where mdsmith is run from. Consider normalizing r.Schema to an absolute OS path based on f.RootDir (when set) and use that consistently for both reading and schema-file identity checks.
	schData, err := readSchemaFile(f, r.Schema)
	if err != nil {
		return append(diags, r.diag(f.Path, 1,
			fmt.Sprintf("cannot read schema %q: %v", r.Schema, err)))
	}

Comment thread internal/rules/crossfilereferenceintegrity/rule.go Outdated
Copilot AI review requested due to automatic review settings April 6, 2026 15:19
@jeduden
jeduden force-pushed the claude/security-hardening-batch-4gvEu branch from 09f3cad to 8099553 Compare April 6, 2026 15:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 7 comments.

Comment thread internal/output/text.go Outdated
Comment thread internal/output/text.go Outdated
Comment thread internal/fix/fix.go
Comment thread internal/fix/fix.go
Comment thread internal/rules/crossfilereferenceintegrity/rule.go Outdated
Comment thread plan/83_security-hardening-batch.md
Comment thread PLAN.md

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Comment thread internal/fix/fix.go
Comment thread plan/83_security-hardening-batch.md Outdated
Comment thread plan/83_security-hardening-batch.md Outdated
Copilot AI review requested due to automatic review settings April 6, 2026 15:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Comment thread internal/rules/requiredstructure/rule.go
Comment thread internal/rules/crossfilereferenceintegrity/rule.go
@jeduden
jeduden force-pushed the claude/security-hardening-batch-4gvEu branch from 230e53c to 1ff20a8 Compare April 6, 2026 20:29
@jeduden
jeduden requested a review from Copilot April 6, 2026 20:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

internal/rules/catalog/rule.go:102

  • Generate() computes catalog injection warnings into diags, but if renderCatalogContent returns an error the function returns only the template-failure diagnostic and drops the previously collected warnings. Consider either appending the template error diagnostic to diags before returning, or delaying injection checking until after template rendering succeeds, so diagnostics are consistent and no work is wasted.
	// Warn about front-matter values that could inject Markdown structure.
	var diags []lint.Diagnostic
	diags = append(diags, checkCatalogInjection(filePath, line, entries)...)

	_, hasRow := params["row"]
	content, err := renderCatalogContent(params, entries, cols, hasRow)
	if err != nil {
		return "", []lint.Diagnostic{makeDiag(filePath, line,
			fmt.Sprintf("generated section template execution failed: %v", err))}
	}

Comment thread internal/rules/crossfilereferenceintegrity/rule.go Outdated
@jeduden
jeduden force-pushed the claude/security-hardening-batch-4gvEu branch from 1ff20a8 to 7fa0f0e Compare April 6, 2026 20:40
@jeduden
jeduden requested a review from Copilot April 6, 2026 20:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

internal/rules/requiredstructure/rule.go:87

  • readSchemaFile reads the schema via f.RootFS, but downstream logic still treats r.Schema as an OS path: parseSchema(schData, r.Schema) uses schemaPath for resolving schema <?include?> fragments (which currently use os.ReadFile), and isSchemaFile(f.Path, r.Schema) uses os.Stat/Abs on the raw string. If RootFS is set and schema is configured as a root-relative path (the new intended mode), running mdsmith from a subdirectory can break schema includes and fail to recognize/skip the schema file itself. Consider resolving a rooted OS path (e.g., filepath.Join(f.RootDir, clean)) to pass into parseSchema/isSchemaFile, and/or updating schema include reads to use f.RootFS as well so resolution is consistent.
	schData, err := readSchemaFile(f, r.Schema)
	if err != nil {
		return append(diags, r.diag(f.Path, 1,
			fmt.Sprintf("cannot read schema %q: %v", r.Schema, err)))
	}

Comment thread internal/rules/catalog/rule.go Outdated
@jeduden
jeduden force-pushed the claude/security-hardening-batch-4gvEu branch from 7fa0f0e to f2c4d57 Compare April 6, 2026 20:50
@jeduden
jeduden requested a review from Copilot April 6, 2026 20:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Comment thread internal/fix/fix.go
@jeduden
jeduden force-pushed the claude/security-hardening-batch-4gvEu branch from f2c4d57 to e85dd90 Compare April 6, 2026 21:01
@jeduden
jeduden force-pushed the claude/security-hardening-batch-4gvEu branch from cdaafe9 to 7a7123d Compare April 6, 2026 21:07
Copilot AI review requested due to automatic review settings April 6, 2026 21:11
@jeduden
jeduden force-pushed the claude/security-hardening-batch-4gvEu branch from 7a7123d to 523320e Compare April 6, 2026 21:11
@jeduden
jeduden force-pushed the claude/security-hardening-batch-4gvEu branch from 523320e to 618bcce Compare April 6, 2026 21:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated no new comments.

@jeduden
jeduden force-pushed the claude/security-hardening-batch-4gvEu branch from 618bcce to 59f3f7d Compare April 6, 2026 21:19
@jeduden
jeduden requested a review from Copilot April 6, 2026 21:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Comment thread internal/rules/crossfilereferenceintegrity/rule_test.go Outdated
Comment thread internal/rules/crossfilereferenceintegrity/rule_test.go Outdated
Comment thread internal/rules/crossfilereferenceintegrity/rule_test.go
Comment thread internal/rules/crossfilereferenceintegrity/rule_test.go Outdated
@jeduden
jeduden force-pushed the claude/security-hardening-batch-4gvEu branch from 59f3f7d to e9a90a1 Compare April 6, 2026 21:55
@jeduden
jeduden requested a review from Copilot April 6, 2026 22:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated no new comments.

@jeduden jeduden changed the title Security hardening: ANSI sanitization, path traversal, atomic writes Implement security hardening batch (plan 83) Apr 6, 2026
@jeduden
jeduden force-pushed the claude/security-hardening-batch-4gvEu branch from e9a90a1 to 43b5663 Compare April 6, 2026 22:21
Copilot AI review requested due to automatic review settings April 6, 2026 23:11
@jeduden
jeduden force-pushed the claude/security-hardening-batch-4gvEu branch from 43b5663 to a2d62eb Compare April 6, 2026 23:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Comment thread internal/rules/crossfilereferenceintegrity/rule.go Outdated
@jeduden
jeduden force-pushed the claude/security-hardening-batch-4gvEu branch from a2d62eb to d4e2650 Compare April 6, 2026 23:18
@jeduden
jeduden requested a review from Copilot April 6, 2026 23:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Comment thread internal/rules/crossfilereferenceintegrity/rule.go
Six low-risk security improvements:

A. ANSI sanitization: strip all C0/C1 control chars from diagnostic
   header fields (file, message) via sanitizeControl; source lines
   preserve tab via sanitizeSourceLine. Prevents terminal injection
   and output spoofing.

B. Path traversal boundary: add RootDir to lint.File, populate from
   Runner/Fixer. Links resolving outside RootDir are silently skipped
   in cross-file-reference-integrity. Uses filepath.EvalSymlinks to
   prevent symlink-based traversal.

C. Atomic writes: replace os.WriteFile with temp-file-then-rename
   (with fsync) in fix mode to reduce risk of partial writes.

D. Schema read via fs.FS: use fs.ReadFile(f.RootFS, ...) for schema
   reads when available; reject absolute and ../ paths.

E. Catalog injection warning: emit diagnostics when interpolated
   front-matter values contain embedded newlines or ]( sequences.
   Map keys iterated in sorted order for deterministic output.

F. GOMEMLIMIT: set 512 MiB memory limit in run() to bound CUE
   evaluation; respects GOMEMLIMIT env var.

G. Include/catalog size limit deferred to plan 81.

https://claude.ai/code/session_012qG8CzpnHem9aZZNbi4XkR
@jeduden
jeduden force-pushed the claude/security-hardening-batch-4gvEu branch from d4e2650 to e3ef2b8 Compare April 6, 2026 23:24
@jeduden
jeduden requested a review from Copilot April 6, 2026 23:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated no new comments.

@jeduden
jeduden merged commit 357cc49 into main Apr 7, 2026
15 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.

3 participants