Skip to content

Harden secure mode: deny-by-default classification plus env, git, and config fixes - #28

Merged
sonirico merged 1 commit into
masterfrom
security/secure-mode-hardening
Aug 27, 2026
Merged

Harden secure mode: deny-by-default classification plus env, git, and config fixes#28
sonirico merged 1 commit into
masterfrom
security/secure-mode-hardening

Conversation

@sonirico

Copy link
Copy Markdown
Owner

What

Harden secure mode. Inverts the executable model to deny-by-default classification (closing GHSA-gg85-6grh-63fp) and closes a batch of weaknesses found by auditing that change with three independent security passes.

Classification (GHSA-gg85-6grh-63fp)

An allowlisted executable now runs only if it is a data-only utility or governed by an argument policy. Command wrappers (env, timeout, nice, xargs, setsid, ...) and any unclassified binary are rejected even when allowlisted, so there is no denylist of "dangerous binaries" to keep chasing.

  • uniq gains a policy that rejects its positional output file (uniq in out = arbitrary write).
  • tar is dropped from the governed set: its useful modes all write a caller-chosen path (tar -cf <path>), so it is not classifiable as safe. Allowlisting it is now rejected as unclassified.

Executor

  • Minimal child environment. The server env and any .env are no longer inherited. cat /proc/self/environ / env can no longer exfiltrate secrets exported by the client or loaded at startup.
  • git repo-local RCE closed. git reads program-executing instructions from .git/config and .gitattributes, invisible to any argv check. core.fsmonitor is disabled and diff/show/blame/log get --no-ext-diff/--no-textconv injected, so a hostile repository's diff.external or textconv driver cannot run during a read-only command.
  • max_output_size enforced at write time, cancelling the child on overflow instead of checking after it has already filled memory (cat /dev/zero no longer OOMs the server).

Validator / config

  • Relative argv[0] with a separator rejected (./ls): it was existence-checked against one directory and executed relative to another.
  • NUL in the command string rejected (the parser silently dropped it, so the audit log diverged from what ran).
  • blocked_patterns/blocked_commands matched against the resolved argv, closing a split-quote evasion (cat /etc/pass"wd").
  • Config loader fails closed: a file that omits keys keeps the secure defaults, and enabled: false requires MCP_SHELL_ALLOW_UNSAFE.

Known, documented gap

branch/tag/symbolic-ref/reflog can still mutate refs within the repository git runs in. Bounded to the repo (no arbitrary-path write, no execution), lower severity, noted in SECURITY.md.

Tests

Every fix landed red-first. The security-critical ones (git repo-config neutralisation, output cap, blocked_patterns-on-argv) were mutation-checked: reverting the fix turns the test red. go test -race ./... is green.

Notes

  • Behaviour changes (relative-path/NUL rejection, minimal env, git flag injection, config fail-closed, tar dropped) warrant a minor version bump (next tag v0.8.0).
  • The deploy/CI check will be red: the account is out of GitHub Actions minutes. That is billing noise, not a regression. Local gate (go test -race ./..., go vet) is green.

…g fixes

Invert the executable model to deny-by-default classification and close a
batch of secure-mode weaknesses found while auditing that change.

Classification (GHSA-gg85-6grh-63fp): an allowlisted executable now runs only
if it is a known data-only utility or governed by an argument policy. Command
wrappers (env, timeout, nice, xargs, ...) and any unclassified binary are
rejected even when allowlisted. uniq gains a policy that rejects its positional
output file; tar is not classifiable (its useful modes all write a caller-chosen
path) and is dropped from the governed set.

Executor:
- Run children with a minimal environment. The server environment and any .env
  are no longer inherited, so an allowlisted reader (cat /proc/self/environ,
  env) cannot exfiltrate secrets.
- Neutralise git repo-local config that executes programs: core.fsmonitor is
  disabled and diff/show/blame/log get --no-ext-diff/--no-textconv injected, so
  a hostile repository's diff.external or textconv driver cannot run.
- Enforce max_output_size at write time and stop the child on overflow instead
  of after it has already filled memory.

Validator and config:
- Reject a relative argv[0] containing a path separator (./ls): it was
  existence-checked against one directory and executed relative to another.
- Reject NUL in the command string (the parser silently dropped it, so the
  audited command differed from what ran).
- Match blocked_patterns/blocked_commands against the resolved argv, closing a
  split-quote evasion (cat /etc/pass"wd").
- Fail closed on config load: a file that omits keys keeps the secure defaults,
  and enabled:false requires MCP_SHELL_ALLOW_UNSAFE.

SECURITY.md documents the threat model, secure-mode guarantees, known gaps and
the scope for vulnerability reports.
@sonirico
sonirico merged commit 1c093e7 into master Aug 27, 2026
1 check 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.

1 participant