Repository navigation
fix(macos): expand write denies in both spellings and check structural glob containment - #791
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Unsound glob containment checks can leave denied files readable and weaken unlink protection, while the write-deny regression is masked by read-deny rules and does not establish that the fix works.
3 open issues
What changed in this PR
This PR addresses #790 by updating macOS Seatbelt profile generation to protect symlinked write-deny targets and reduce incorrect glob carve-outs.
Changes:
- Expand write-deny entries to include both original and resolved path spellings.
- Add a second wildcard sample when checking deny-glob coverage.
- Add regression tests for symlink write protection and narrower deny globs.
| File | Description |
|---|---|
test/sandbox/trailing-slash-deny-spellings.test.ts |
Adds symlink and glob carve-out regression tests. |
src/sandbox/macos-sandbox-utils.ts |
Expands write-deny path spellings and updates glob coverage probes. |
Review effort: Balanced
| denyRegex.test(entrySamplePath(entry, 'x', 'x')) && | ||
| denyRegex.test(entrySamplePath(entry, 'Q7_', '9')) |
There was a problem hiding this comment.
Addressed in 42cc65c: replaced sample probing in denyGlobCovers with a sound structural containment check over parseGlob tokens (globEntryAtoms + rangesCover), and added /work/pub/[a-zA-Z]* under allowWithinDeny: ['/work/pub/*'] to the regression suite.
| anchor + | ||
| entry.path | ||
| .slice(anchor.length) | ||
| .replace(/\[[^\]]*\]|\*+|\?/g, m => (m[0] === '*' ? starFill : charFill)) |
There was a problem hiding this comment.
Addressed in 42cc65c: character-class sets (GlobTokenKind.Set) are now compared by interval containment (rangesCover), and added the denyOnly: ['/work/pub/[a-z]*'] + allowWithinDeny: ['/work/pub/[a-z]safe*'] regression test.
| readConfig: { | ||
| denyOnly: [linkMcp], | ||
| }, |
There was a problem hiding this comment.
Addressed in 42cc65c: set readConfig: undefined in the symlink write-deny test to isolate generateWriteRules from generateReadDenyUnlinkRules.


Fixes #790
Summary
This PR resolves two macOS Seatbelt profile compilation bugs in
src/sandbox/macos-sandbox-utils.tsfollowingcc3d45c:Expand
denyEntriesingenerateWriteRulesthroughinBothSpellings('deny', entry):resolveReadConfigexpands read-deny entries throughcallerEntries(...).flatMap(entry => inBothSpellings('deny', entry))so that symlinked deny paths emit both the link spelling and the resolvedrealpathspelling.generateWriteRulespreviously omittedinBothSpellings('deny', entry), leaving the resolved target of an intra-workspace symlink (e.g.,<workspace>/.mcp.json -> <workspace>/real_config/mcp.jsonor a symlink indenyWithinAllow) writable underallowOnly: [<workspace>].denyEntriesthrough.flatMap(entry => inBothSpellings('deny', entry))ensures both spellings are emitted in(deny file-write* ...)and move-blocking rules.Check structural glob containment in
denyGlobCovers:denyGlobCoverspreviously testeddenyRegex.test(entrySamplePath(entry))(whereentrySamplePathsubstituted every glob wildcard with'x'), causingdenyGlobCoversto returntruewhenever a narrowerdenyOnlyglob happened to match'x'(such as/work/pub/x*,/work/pub/[u-z]*,/work/pub/[a-zA-Z]*, or/work/pub/?) under a broaderallowWithinDenyglob (/work/pub/*).globEntryAtoms+rangesCover) whenentry.globistrueensuresdenyGlobCoversonly carves out an allow/writeRoot entry when the subtree-extended deny glob genuinely covers the allow glob's wildcard region.Test Plan
test/sandbox/trailing-slash-deny-spellings.test.tscovering:denyWithinAllowemitting both link and resolved target spellings in the; File writesection.denyOnlyglobs (/work/pub/x*,/work/pub/[u-z]*,/work/pub/[a-zA-Z]*,/work/pub/?) underallowWithinDeny: ['/work/pub/*']remaining un-carved and landing after the allow rule./work/pub/[a-z]safe*underdenyOnly: ['/work/pub/[a-z]*']) still being carved out.