Skip to content

fix(plugin): refuse /tmp fallback and verify path ownership before exec (D-1396) - #983

Open
acamarata wants to merge 5 commits into
mainfrom
dw6-cli-2
Open

acamarata wants to merge 5 commits into
mainfrom
dw6-cli-2

Conversation

@acamarata

Copy link
Copy Markdown
Collaborator

D-1396: plugin router /tmp fallback and missing ownership checks

What changed

  • pluginBinDir() now returns an error when HOME is unset instead of falling back to /tmp/.nself/plugins/bin, which any local user could populate with a malicious binary that cron or systemd would execute with the caller's secrets.
  • ProxyBinaryAt walks every component of the resolved plugin path up to the plugins root and verifies each is owned by the current uid and has no group- or world-write bits set before exec.
  • All callers of pluginBinDir and PluginBinDir updated for the new (string, error) signature.
  • Regression test TestPluginBinDirRefusesWithoutHOME proves refusal without HOME.

Verification

  • go vet ./internal/plugin/...: passed
  • go build ./...: passed
  • go test ./internal/plugin/... -count=1: all passed including new regression test

Other rows assessed

  • D-0851: BLOCKED — requires contract v1.1 epic decision (multi-JWT file format, SDK change across released versions). Not a local fix.
  • D-1208: ALREADY-FIXED — pnpm install --frozen-lockfile succeeds on origin/main; lockfile contains matching overrides block.
  • D-0841: BLOCKED — requires P7-PLUG-71 allowlist data generation; floor.go has no allowlist mechanism yet.
  • D-0922: BLOCKED — source is nself-org/plugins repo, not cli; no gate_run.go in this repo.

@acamarata

Copy link
Copy Markdown
Collaborator Author

Fixed all four review findings in f2b7f93:

  1. Windows build: moved syscall.Stat_t/uid check into router_unix.go and a no-op router_windows.go with build tags; GOOS=windows go build ./internal/plugin/ now succeeds.
  2. Hint formatting & blank lines: restored the blank line before the install hint, the double newline (\n\n), and the two-space indent before %s to match origin/main; also restored the blank lines around L111, L120, L149 and in runPluginCommand.
  3. Ownership tests: added TestVerifyPluginPathOwnershipRefusesUnsafe (world-writable, foreign-gid group-writable, foreign-uid) and TestProxyBinaryAtRefusesUnsafePath; both fail if the ownership check is removed. Foreign-gid/uid sub-tests skip gracefully when chown is not permitted (e.g. macOS temp dirs).
  4. umask 002: group-write is now allowed when the directory's gid matches the user's primary group (user-private-group / umask 002 installs); foreign-gid group-write is refused with a message suggesting chmod g-w.

All plugin tests pass (go test -mod=vendor ./internal/plugin/).

@acamarata

Copy link
Copy Markdown
Collaborator Author

Fixed P1 finding in router_unix.go: verifyPluginPathOwnership now rejects any group-writable path component regardless of GID, closing the macOS shared-group (e.g. staff) bypass. Added a same-GID 0775 regression case in TestVerifyPluginPathOwnershipRefusesUnsafe; full plugin module tests pass with -mod=vendor. Commit 5830906.

…ec (D-1396)

pluginBinDir now returns an error when HOME is unset instead of falling
back to /tmp/.nself/plugins/bin, which any local user could populate
with a malicious binary that cron or systemd would execute with the
caller's secrets. ProxyBinaryAt additionally walks every component of
the resolved plugin path up to the plugins root and verifies that each
is owned by the current uid and has no group- or world-write bits set.

All callers of pluginBinDir and PluginBinDir are updated for the new
(string, error) signature. A regression test proves refusal without
HOME.
…ng, allow private-group write

- Move verifyPluginPathOwnership uid/stat logic into router_unix.go and
  router_windows.go with build tags so GOOS=windows builds succeed.
- Restore the blank line before the install hint and the two-space indent
  plus double newline in the hint text to match origin/main.
- Allow group-write when the directory's gid matches the user's primary
  group (umask 002 / user-private-group installs); refuse foreign-gid
  group-write with a message suggesting chmod g-w.
- Add tests that refuse world-writable and foreign-gid group-writable path
  components and wire the check through ProxyBinaryAt.
…perm (D-1396)

internal/plugin was 102 lines over its package-size ceiling. The path
resolution and ownership check now live in pluginperm (no errs import, so
the errs fan-in ceiling holds) and the nself-<slug> binary check reuses the
pluginname slug grammar. No ceiling is raised.
@acamarata

Copy link
Copy Markdown
Collaborator Author

Rebased onto origin/main and fixed the red CI (acbee64). The review findings text was empty, so I worked from the failing checks.

  • gofmt: router.go, router_test.go and the unix/windows router files were unformatted, which failed TestGofmtClean and the gofmt job. A malformed else-block in ProxyCommandWithHint is fixed too.
  • Package-size ratchet: internal/plugin was 102 lines over its ceiling. The path resolution and ownership check now live in a new internal/pluginperm package. It does not import errs, so the errs fan-in ceiling still holds. The nself- binary check reuses the pluginname slug grammar. No ceiling was raised.
  • Test: removing the ownership check from pluginperm.Resolve makes TestProxyBinaryAtRefusesUnsafePath fail; with it restored, the test passes.
  • Local: gofmt clean, build and vet pass (also GOOS=windows), tests pass for plugin/..., pluginname, pluginperm and repoqa.
  • Not done: the other /tmp fallbacks in plugin/paths.go (cache dir and license key path) are outside D-1396 and untouched. The Docker-dependent tests and the nself-ci gates go:test and go:sdk-remote-mutation were not run locally.

This branch has not been deployed

No deployments
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