Skip to content

Harden the installer engine against untrusted packs + lifecycle data loss - #53

Closed
tmcfarlane wants to merge 1 commit into
feat/tauri-desktopfrom
fix/pack-engine-hardening
Closed

tmcfarlane wants to merge 1 commit into
feat/tauri-desktopfrom
fix/pack-engine-hardening

Conversation

@tmcfarlane

Copy link
Copy Markdown
Owner

Hardens the installer engine (packages/core) before it starts accepting third-party packs. Today pack.json content is installed with zero validation, and several lifecycle paths silently lose user data. Six verified fixes, each with an adversarial regression test — engine coverage goes 4 → 11 tests.

Stacked on #52 (base feat/tauri-desktop).

Fixes

# Severity Fix
1 critical Path traversal → arbitrary file write / RCE. skills[].name was joined straight into the write path, so name: "../../.." escaped ~/.cursor (e.g. into ~/.ssh/authorized_keys). Now: slug-validate pack.id + skill names at load time (covers the CLI and the bridge), plus a containment guard (assertWithin) that refuses any destination resolving outside the tool dir.
2 high Git-hook trust bypass. The project-scope pre-commit hook (runs the pack's shell on every commit) was gated on a substring in uninstall.extraPaths, ignoring permissions.gitHooks — the field the CLI/GUI show users. Now gated on the declared permission; a pack showing gitHooks:false can't install one.
3 high Silent overwrite on first install. A pre-existing untracked user file colliding with a pack path was clobbered with no backup. Now classified userModified and backed up to *.omc-bak.
4 high Upgrade orphans. Files the previous version shipped but the new one drops lingered forever and vanished from the lock (uninstall couldn't remove them). Now removed on upgrade (only files we still own) and cleared from the lock.
5 medium Uninstall data loss. Uninstall rm'd every recorded file unconditionally, including ones the user edited. Now skips files whose content no longer matches what we recorded and reports them as keptModified.
6 medium Lockfile integrity. writeLock now writes to a temp file + atomic rename, so a crash / concurrent writer can't leave a truncated lock.json (which readLock treats as empty, orphaning every prior install).

Tests (packages/core/test/hardening.test.mjs)

assertWithin unit • skill-name traversal rejected at load • bad pack id rejected • first-install collision backs up the user file • upgrade removes + de-lists the orphan • uninstall preserves a user-edited file • git hook honors permissions.gitHooks.

Verification

  • Full engine suite: 11/11 pass (node --test packages/core/test/*.test.mjs).
  • Existing parity / backup / uninstall / git-hook tests unchanged and green.
  • scripts/validate-packs.mjs green; the shipped team-avatar pack (gitHooks:true, valid slugs) is unaffected.

Not in scope (follow-ups)

  • sourceRoot can still point outside the pack dir (the flagship relies on "../.."); the containment guard keeps writes inside the tool dir regardless, but the real fix is making packs self-contained.
  • Registry integrity (per-pack hashes/signatures) and a shellcheck/permissions-consistency lint in the validator are the next bricks of the trust chain before opening submissions.

🤖 Generated with Claude Code

…le data loss

The engine installs pack.json content with zero validation and will soon take third-party packs. Six verified fixes, each with an adversarial regression test (engine test coverage goes 4 -> 11):

- Path traversal (critical): validate pack id + skill names against the slug pattern at load time (blocks '../..' payloads for the CLI and bridge), plus a containment guard that refuses any destination resolving outside the tool dir. Was: skills[].name joined straight into the write path -> arbitrary file write / RCE.
- Git-hook trust bypass: gate the project-scope pre-commit hook on the DECLARED permissions.gitHooks (the field the CLI/GUI render), not a substring in uninstall.extraPaths. A pack showing gitHooks:false can no longer install a hook that runs its shell on every commit.
- Silent data loss on first install: a pre-existing untracked user file that collides with a pack path is now classified userModified and backed up to *.omc-bak instead of being clobbered.
- Upgrade orphans: files the previous version shipped but the new one drops are now removed (only the ones we still own) and cleared from the lock, restoring the 'leave no trace' guarantee.
- Uninstall data loss: skip deleting a file whose content no longer matches what we recorded (user edited it); report it as keptModified.
- Lockfile integrity: write via temp-file + atomic rename so a crash / concurrent writer can't leave a truncated lock.json (which readLock treats as empty, orphaning every prior install).

Existing parity/backup/uninstall/git-hook tests still pass; team-avatar (gitHooks:true) is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tmcfarlane
tmcfarlane requested a review from bastien70 as a code owner July 2, 2026 16:27
@tmcfarlane tmcfarlane closed this Oct 20, 2026
@tmcfarlane
tmcfarlane deleted the fix/pack-engine-hardening branch October 20, 2026 11:28
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