Skip to content

fix: harden path sandboxing with symlink protection, safe defaults, and sensitive file guards#209

Merged
anandgupta42 merged 9 commits into
mainfrom
fix/security-hardening-v2
Mar 17, 2026
Merged

fix: harden path sandboxing with symlink protection, safe defaults, and sensitive file guards#209
anandgupta42 merged 9 commits into
mainfrom
fix/security-hardening-v2

Conversation

@anandgupta42

Copy link
Copy Markdown
Contributor

What does this PR do?

Hardens the security posture of Altimate Code's path sandboxing and permission system, addressing the same class of vulnerabilities that led to CVEs in Codex (GHSA-w5fx-fh39-j5rw, CVSS 8.6) and Claude Code (CVE-2025-54794, CVSS 7.7). Reviewed by 6 AI models (Claude, GPT 5.2 Codex, Gemini 3.1 Pro, Kimi K2.5, MiniMax M2.5, GLM-5).

Five areas of improvement:

  1. Symlink escape protectionFilesystem.containsReal() resolves symlinks via realpathSync before checking containment. Rejects paths with unresolved .. segments to prevent divergence between realpathSync (lexical .. normalization) and kernel behavior (follows symlink then applies ..). Also adds isAbsolute(rel) check for Windows cross-drive bypass.

  2. Safe permission defaults — Destructive shell/git commands (rm -rf, git push --force, git reset --hard) now prompt before execution instead of running silently. Database DDL (DROP DATABASE, DROP SCHEMA, TRUNCATE) is blocked entirely. Users can override in config.

  3. Sensitive file guards — New assertSensitiveWrite() check on write, edit, apply_patch, and move operations that prompts before modifying .env*, .ssh/, .aws/, .git/, credential files, and private keys (.pem, .key) — even inside the project boundary. Case-insensitive on macOS/Windows.

  4. Critical fix: realpathSync vs kernel divergence — Gemini 3.1 Pro discovered that realpathSync("project/symlink/..") returns project/ (lexical) while the kernel writes to the parent of the symlink target (outside project). Fixed by rejecting any unresolved path containing .. segments.

  5. Documentation — Updated SECURITY.md, permissions docs with 3 recommended configs and rule ordering guidance, and security FAQ with new sections on sensitive file prompts, default command protections, and best practices.

UX Impact

Change User Impact What users will notice
Symlink-aware path checks None Transparent — only blocks symlinks pointing outside the project
Windows cross-drive fix None Only relevant on Windows edge case
.. segment rejection None Normal tools already strip .. — only catches crafted paths
Bash defaults: destructive → ask Low rm -rf, git push --force, etc. now prompt instead of running silently. "Allow always" available.
Bash defaults: database DDL → deny Low DROP DATABASE, TRUNCATE blocked entirely. Override in config if needed.
Sensitive file prompts Medium First edit of .env, .ssh/, .aws/, credential files prompts. "Allow always" per-file for the session.

Key UX decisions:

  • ask instead of deny for shell/git commands — Blocking rm -rf ./build or git push --force after rebase would break common workflows. Prompting lets users approve safely.
  • "Allow always" for sensitive files — Users click once per .env file per session, not on every edit.
  • .github NOT in sensitive dirs — CI/CD workflow editing is a core use case; prompting every time would cause approval fatigue.
  • Database DDL is the only hard blockDROP DATABASE, DROP SCHEMA, TRUNCATE are almost never intentional in agent context.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Documentation update

Issue for this PR

Closes #202

How did you verify your code works?

  • 105 tests passing (27 unit + 73 e2e + 5 existing external-directory tests)
  • E2E tests use real filesystem operations (symlinks, tmp dirs, real path resolution)
  • Test coverage includes:
    • Symlink escape: file symlink, directory symlink, chained symlinks, relative symlinks, symlink/../ kernel divergence attack
    • Path traversal via File.read/File.list
    • Absolute path escape and prefix collision
    • Non-git project worktree safety
    • Sensitive dir/file detection: .git, .ssh, .aws, .env*, credentials, private keys
    • Case-insensitive detection on macOS/Windows
    • Certificate extension detection (.pem, .key, .p12, .pfx)
    • assertSensitiveWrite prompting for sensitive files, no-op for normal files
    • Bash deny defaults evaluation: database DDL denied, shell commands prompted
    • User config override merge semantics (last-match-wins)
    • Windows backslash path handling
  • Typecheck passes (4/4 turbo tasks)
  • Multi-model code review by 6 models with all findings addressed

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have added tests that prove my fix is effective
  • New and existing tests pass locally with my changes

🤖 Generated with Claude Code

anandgupta42 and others added 7 commits March 16, 2026 19:05
…nd sensitive file guards

- Add `Filesystem.containsReal()` with `realpathSync` to prevent symlink escape attacks
  (same class of bug as Codex GHSA-w5fx-fh39-j5rw and Claude Code CVE-2025-54794)
- Add `isAbsolute(rel)` check to `Filesystem.contains()` for Windows cross-drive bypass
- Update `Instance.containsPath()` to use symlink-aware `containsReal()`
- Add safe permission defaults: deny `rm -rf`, `git push --force`, `git reset --hard`,
  `DROP DATABASE`, `TRUNCATE` out of the box
- Add `Protected.isSensitiveWrite()` to detect writes to `.git/`, `.ssh/`, `.aws/`,
  `.env*`, credential files even inside the project boundary
- Add `assertSensitiveWrite()` guard to write, edit, and apply_patch tools
- Remove resolved TODO comments from `file/index.ts`
- Update SECURITY.md, permissions docs, and security FAQ with practical guidance
- Add 94 tests including 62 e2e tests covering symlink attacks, path traversal,
  sensitive file detection, and combined attack scenarios

Closes #202

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… paths, TOCTOU docs

- Fix critical bug: bash deny defaults had `"*": "ask"` LAST which overrode deny rules
  due to last-match-wins semantics. Moved `"*": "ask"` to first position so deny rules
  take precedence.
- Fix all doc examples with same ordering bug (security-faq.md, permissions.md)
- Fix `isSensitiveWrite` to use regex split `/[/\\]/` for cross-platform path handling
- Allow per-path "Always" approval for sensitive file writes (reduces approval fatigue)
- Document TOCTOU limitation in `containsReal` JSDoc
- Add doc clarification about last-match-wins rule ordering with examples
- Add tests: bash deny defaults evaluation, user override merge, Windows backslash paths

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nsitive matching, expanded patterns

Fixes from consensus across GPT 5.2, Kimi K2.5, MiniMax M2.5, and GLM-5 reviews:

- Add `assertSensitiveWrite(ctx, movePath)` for move destinations in `apply_patch`
  (CRITICAL: 3 models flagged that moves to `.ssh/`, `.env` bypassed sensitive check)
- Add case-insensitive matching on macOS/Windows for sensitive dirs and files
  (`.GIT/config`, `.SSH/id_rsa` now correctly detected on case-insensitive FS)
- Expand `SENSITIVE_FILES` with `.htpasswd`, `.pgpass`
- Add `SENSITIVE_EXTENSIONS` for private keys: `.pem`, `.key`, `.p12`, `.pfx`
- Add tests: case-insensitive matching, certificate extensions, credential files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…on divergence

Gemini 3.1 Pro found that `realpathSync` and the OS kernel disagree on
`symlink/../file.txt`:
- `realpathSync("project/link/..")` → `project/` (lexical normalization)
- `writeFile("project/link/../f")` → writes to parent of symlink TARGET (kernel)

This means `containsReal` would approve a write that the OS places OUTSIDE
the project boundary. The fix rejects any unresolved path containing `..`
segments, since their behavior through symlinks is fundamentally unpredictable
at the application level.

Also adds `.github` to `SENSITIVE_DIRS` (workflow injection vector).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… from sensitive dirs

UX impact evaluation of each change:

1. **Bash defaults softened**: Changed destructive shell/git commands from
   `deny` (blocked silently) to `ask` (prompted). `rm -rf ./build` and
   `git push --force` after rebase are legitimate workflows — blocking them
   without a prompt is poor UX. Database DDL (`DROP DATABASE`, `TRUNCATE`)
   stays `deny` since it's almost never intentional in agent context.

2. **Removed `.github` from sensitive dirs**: Editing CI/CD workflows is a
   core use case. Prompting on every workflow edit would cause severe
   approval fatigue.

3. **Expanded FAQ**: Added "Why am I being prompted to edit .env files?"
   with table of protected patterns and guidance on "Allow always".
   Added "What commands are blocked or prompted by default?" with clear
   table showing which commands prompt vs block. Reordered best practices
   to lead with "work on a branch" (most effective, least friction).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…icky DDL deny rules

Fixes two issues flagged by Sentry automated review:

1. `assertSensitiveWrite` now uses `permission: "sensitive_write"` instead of
   `"edit"`, preventing agents with `edit: "allow"` from silently bypassing
   sensitive file prompts for `.env`, `.ssh/`, `.aws/`, etc.

2. Database DDL deny rules (`DROP DATABASE`, `DROP SCHEMA`, `TRUNCATE`) are now
   merged as a `safetyDenials` layer AFTER user/agent configs via `userWithSafety`.
   This ensures wildcard `bash: "allow"` in agent configs cannot override these
   denials (last-match-wins). Users who need to override must use specific patterns
   like `"DROP DATABASE test_db": "allow"`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@anandgupta42 anandgupta42 added bug Something isn't working priority:high High priority labels Mar 17, 2026
@claude

claude Bot commented Mar 17, 2026

Copy link
Copy Markdown

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review.

… mismatches

- Update `build agent has correct default properties` test: bash now
  defaults to "ask" (was "allow") per safety defaults
- Fix `config/config.ts` marker mismatch: add missing `altimate_change end`
  after auto-enhance prompt config (9 starts vs 8 ends → 9/9)
- Fix `tool/skill.ts` marker mismatch: remove orphaned `altimate_change end`
  at EOF (6 starts vs 7 ends → 6/6)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

Comment thread packages/opencode/src/agent/agent.ts
…patterns

- Resolve merge conflicts in skill.ts and commit.txt
- Add lowercase DDL deny patterns (drop database, truncate) to both defaults
  and safetyDenials — Wildcard.match is case-sensitive on Linux/macOS
- Add tests verifying lowercase DDL commands are denied

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@anandgupta42

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Mar 17, 2026

Copy link
Copy Markdown

⚠️ Code review skipped — your organization's overage spend limit has been reached.

Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit in Settings → Usage.

Once credits are available, comment @claude review on this pull request to trigger a review.

@anandgupta42

Copy link
Copy Markdown
Contributor Author

@claude review

@anandgupta42 anandgupta42 merged commit 0449a8a into main Mar 17, 2026
7 checks passed
@dev-punia-altimate

Copy link
Copy Markdown
Contributor

✅ Tests — All Passed

TypeScript — passed

cc @anandgupta42
Tested at 06010942 | Run log | Powered by QA Autopilot

@anandgupta42 anandgupta42 deleted the fix/security-hardening-v2 branch March 25, 2026 02:14
anandgupta42 added a commit that referenced this pull request Jun 26, 2026
… (shipped security regression)

Found by the confidence audit (scoping the SHIPPED surface). Commit #209 (0449a8a)
hardened the project-boundary check to be symlink-aware via Filesystem.containsReal
(realpathSync) to stop symlink-escape attacks (CVE-class GHSA-w5fx-fh39-j5rw /
CVE-2025-54794). The v1.17.9 merge introduced a NEW lexical copy of containsPath in
project/instance-context.ts (FSUtil.contains, no symlink resolution) and rewired
tool/external-directory.ts — which gates grep/glob/ls and the external_directory
permission — to that copy. Instance.containsPath (project/instance.ts) kept the
protection, so the regression was invisible: the #209 security-e2e tests only exercise
Instance.containsPath, not this copy, so CI stayed green.

Impact (shipped): an in-project symlink to an external dir (e.g. `proj/x -> ~/.ssh`)
passes the lexical check, so grep/glob search OUTSIDE the project WITHOUT the
external_directory prompt. Confirmed: `rg` with a symlink-dir cwd reads the external
target.

Fix: instance-context.containsPath now uses Filesystem.containsReal (resolves real
paths, walks to nearest existing ancestor for write targets, rejects ".."), matching
Instance.containsPath. New regression test exercises THIS copy with a real on-disk
symlink (mutation-checked: fails against lexical contains). typecheck 13/13;
security-e2e + new test 76 pass; marker 160; branding clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
anandgupta42 added a commit that referenced this pull request Jun 26, 2026
… security regression #2)

Found by the differential audit (main vs merge). #209 added assertSensitiveWrite — a
SEPARATE "sensitive_write" permission prompt before writing/editing/moving into
credential/VCS/security locations (.git/, .ssh/, .aws/, .env*, private keys, ...), even
inside the project boundary, so an agent with `edit: "allow"` (the permissive builder
default) cannot silently bypass it. The v1.17.9 merge dropped the wrapper AND all 4 call
sites in write/edit/apply_patch; Protected.isSensitiveWrite survived as dead code. Impact
(HIGH): the agent could overwrite a project-local .env, .git/hooks/*, or a checked-in
private key with NO prompt — an exfiltration/persistence vector.

Why it slipped past CI: the merge inlined a PRIVATE copy of assertSensitiveWrite into
test/file/security-e2e.test.ts, so the suite compiled + passed green while testing the
copy, never the (absent) production path.

Fix:
- Restore assertSensitiveWriteEffect (Effect) + a Promise wrapper in tool/external-directory.ts
  (Effect port of main's; underlying match is the surviving Protected.isSensitiveWrite).
- Re-wire `yield* assertSensitiveWriteEffect(ctx, <path>)` into write.ts, edit.ts, and
  apply_patch.ts (add + move destination) — 4 call sites.
- Un-neuter security-e2e.test.ts: import the production guard instead of a local copy.
- Add a PRESENCE guard (fork-feature-guards.test.ts) asserting the wrapper is defined,
  "sensitive_write" is the permission key, and all three tools call the guard — so a future
  merge that drops the WIRING (not just the function) goes red.

The differential audit's bounded accounting: this is the ONLY remaining shipped #209 drop;
all other fork security behaviors (containReal boundary [fixed], bash deny-defaults, auth,
redaction, OAuth escaping) survive. typecheck 13/13; security-e2e 73 + tool suites 112 pass;
marker 160; branding clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
anandgupta42 added a commit that referenced this pull request Jun 26, 2026
…nfidence audit

- Restore packages/opencode/test/file/path-traversal.test.ts (15 Filesystem.contains
  security cases the v1.17.9 merge dropped); all 15 pass against the merged code. It was
  the ONLY dropped fork security test file (verified via comm of main vs HEAD test trees).
- Add .github/meta/night-run/RELEASE-CONFIDENCE-2026-06-26.md: the consolidated confidence
  map — every issue found this session → fix → gate, the scope (shipped vs v2-core/unshipped),
  the 2 shipped #209 security regressions the differential found + why they slipped past
  Kilo/codex/CI, and the recommended permanent drop-detection gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
anandgupta42 added a commit that referenced this pull request Jun 26, 2026
…m) — shipped regression #3

Found by the differential audit (session/agent/prompt slice). #209-era code loaded the
~11 builtin DE skills (dbt-analyze, dbt-pr-review, cost-report, data-parity, data-viz,
dbt-develop/docs/test, dbt-schema-verify, altimate-setup, ...) into the live skill
registry via `~/.altimate/builtin/` (FS, postinstall) with a binary-embedded
OPENCODE_BUILTIN_SKILLS fallback. The v1.17.9 merge REWROTE skill/index.ts into the live
@/skill module but only registers `customize-opencode` + disk discovery — it never reads
the embedded blob or ~/.altimate/builtin. The loader survived only in the now-orphaned
skill.ts. Impact (HIGH): on Homebrew/Docker/npm-without-postinstall installs, all builtin
DE skills silently vanished from <available_skills>, the Skill tool, and auto-load — core
fork value, no error. The build still embeds them.

Why it slipped past CI: build-integrity.test.ts asserted skill.ts (the ORPHAN) loads the
blob, staying green while the live module didn't.

Fix: restore both loaders in skill/index.ts (FS-first for @references, embedded fallback
via gray-matter), registered before disk discovery so user-disk skills still override.
Retarget the build-integrity test at the LIVE module (skill/index.ts) so the drop can't
recur unseen. typecheck 13/13; skill + build-integrity suites 95 pass; marker 160.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
anandgupta42 added a commit that referenced this pull request Jul 2, 2026
Release-evaluation board (safety perspective) found the #209 sensitive-write
guard was neutralized at runtime. The guard calls
`ctx.ask({ permission: "sensitive_write" })` before writing a sensitive target
(.env / .ssh / .git / ...), but `sensitive_write` was never configured, so it
fell through to the builder's `"*": "allow"` default and silently auto-approved
— defeating the guard the fork deliberately restored. Pre-existing (present on
main too), not a merge regression, but a real bypass.

Default `sensitive_write: "ask"` in the agent defaults so sensitive writes
prompt (users can still override to "allow"). Write-restricted agents
(analyst/reviewer/plan) keep their later `"*": "deny"`. Regression test asserts
builder=ask, analyst/reviewer=deny.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working contributor needs:issue priority:high High priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: harden path sandboxing — symlink escape, cross-drive bypass, no OS sandbox

2 participants