Skip to content

Harden config file loading: size cap, strict fields, symlink reject#131

Merged
JAORMX merged 1 commit intomainfrom
sec/config-file-hardening
Apr 17, 2026
Merged

Harden config file loading: size cap, strict fields, symlink reject#131
JAORMX merged 1 commit intomainfrom
sec/config-file-hardening

Conversation

@JAORMX
Copy link
Copy Markdown
Contributor

@JAORMX JAORMX commented Apr 17, 2026

Summary

Closes a LOW security finding with three parts. The config loaders (global ~/.config/broodbox/config.yaml, workspace .broodbox.yaml, --mcp-config file) previously:

  1. Had no size cap — a 2 GiB malicious .broodbox.yaml would OOM bbox before it could warn.
  2. Silently dropped unknown YAML fields — a typo like mcp.athz.profile: observe (should be authz) failed open to full-access. Security-critical typos fell back to permissive defaults with no error.
  3. Followed symlinks for the workspace-local config — a malicious repo could point .broodbox.yaml at any readable path on the host.

Changes

New internal/infra/configfile package with ReadFile (1 MiB cap, optional symlink reject, non-regular-file reject) and DecodeStrict (yaml.Decoder.KnownFields(true)). Three loaders wire it in:

Loader Size cap Strict fields Symlink reject Rationale
Loader.Load (global) Operator owns the path; symlink to a team dotfiles dir is legitimate
LoadFromPath (workspace) Repo controls the path; all hardening applies
LoadMCPFileConfig (--mcp-config) Operator-supplied CLI flag

Plus one UX-safety addition flagged by reviews: all three loaders reject non-regular files (FIFOs, sockets, device nodes). Without this, a FIFO planted at .broodbox.yaml would cause os.Open to block indefinitely waiting for a writer, hanging bbox with no error.

Error-handling stays asymmetric (established in prior PR): global config errors are fatal (user owns the file); workspace errors are warn-and-skip (malicious repo cannot DOS the session).

Docs: extended docs/USER_GUIDE.md section on per-workspace config with the new file rules so users hitting the symlink rejection know to copy templates instead of linking.

Test plan

  • task fmt && task lint && task test — all green
  • Unit tests for each hardening: typo rejection (mcp.athz.profile), oversized file (2 MiB), workspace symlink, FIFO at workspace path, MCP config typo/oversized
  • Negative control: global config symlinked to a regular file still loads
  • Headless end-to-end:
    • .broodbox.yaml typo → Warning: failed to load local config ...: parsing config file ...: yaml: unmarshal errors: line N: field athz not found (bbox continues with global)
    • Oversized .broodbox.yamlWarning: ...: config file ... is too large: 2097152 bytes (limit 1048576) (bbox continues)
    • .broodbox.yaml symlink → Warning: ...: symlinks are not allowed for workspace-local config (points to "target.yaml") (bbox continues)
    • FIFO at .broodbox.yamlWarning: ...: not a regular file (mode=prw-r--r--) (bbox continues; does not hang)

🤖 Generated with Claude Code

Three gaps in the config loaders:

1. No size cap. A 2 GiB `.broodbox.yaml` in a malicious repo would
   OOM bbox before it could warn. `os.ReadFile` slurps the whole
   file unconditionally.
2. No strict-unknown-fields. A typo like `mcp.athz.profile: observe`
   (should be `authz`) was silently dropped, and the operator fell
   back to the permissive default. Security-critical typos failed
   open.
3. Workspace-local `.broodbox.yaml` followed symlinks. A malicious
   repo could point the workspace config file at any readable path
   on the host, confusing the trust boundary between workspace and
   global config.

Add a shared `internal/infra/configfile` package with `ReadFile` and
`DecodeStrict`. Three loaders use it:

- internal/infra/config/loader.go: global Load() — size cap + strict.
  Symlinks allowed because the operator picks the path.
- internal/infra/config/loader.go: LoadFromPath() — size cap + strict +
  symlink reject + non-regular-file reject. Workspace path is
  repo-controllable.
- internal/infra/mcp/configloader.go: LoadMCPFileConfig — size cap +
  strict. --mcp-config is operator-supplied, no symlink reject.

Size cap is 1 MiB (real configs are single-digit KiB; the limit exists
only to bound DoS, not to be restrictive). Strict mode uses
yaml.Decoder.KnownFields(true) so unknown keys produce an error naming
the offending field and line number.

Non-regular files (FIFOs, sockets, device nodes) are rejected before
Open, so a malicious or accidental FIFO at `.broodbox.yaml` fails
cleanly with "not a regular file" instead of hanging bbox on a blocking
open.

Document the new file rules in docs/USER_GUIDE.md so users hitting
the symlink rejection know to copy the template rather than link it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JAORMX JAORMX merged commit 215e56d into main Apr 17, 2026
8 checks passed
@JAORMX JAORMX deleted the sec/config-file-hardening branch April 17, 2026 08:40
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.

1 participant