Skip to content

Security: bgorzelic/ghostlink

Security

docs/SECURITY.md

GhostLink — SECURITY.md

Phase 1 Security Model (Local Only)

Path Containment (Sandbox)

All file-bearing tools (repo.read_file, repo.apply_patch, repo.search, git.diff) route paths through src/core/policy/sandbox.ts:

  • Path canonicalization: resolve() resolves relative paths against repo root
  • Traversal rejection: Paths resolving outside repo root are rejected (e.g., ../../etc/passwd)
  • Symlink escape detection: realpathSync verifies resolved symlink targets stay within sandbox
  • Null byte rejection: Paths containing \0 are rejected before any filesystem operation
  • Absolute path containment: Absolute paths outside repo root are rejected
  • Max path length: 4096-byte limit on input paths

Implementation: Sandbox.resolve() — called before every filesystem operation.

No Arbitrary Shell

  • repo.run uses spawn() with shell: false — args are passed as arrays, never interpolated
  • repo.search uses execFile() (no shell by default) — ripgrep invoked directly
  • git.status and git.diff use execFile() (no shell by default) — git invoked directly
  • No shell-based execution anywhere in the codebase

Command Allowlists (repo.run)

  • Fixed enum of allowed commands: test, lint, typecheck, build, smoke
  • Per-command allowlisted argument sets (e.g., test allows --reporter=dot)
  • Disallowed arguments rejected before spawn
  • Minimal environment: only PATH, HOME, USER, LANG, TMPDIR, NODE_ENV passed

Output Flooding Prevention

Each tool enforces output caps:

Tool Cap Mechanism
repo.search 200 results max max_results parameter, capped at 200
repo.read_file 10MB max max_bytes parameter, capped at 10MB
repo.apply_patch N/A Input-bounded (patch size)
repo.run 200KB stdout + 200KB stderr Buffer-based collection with byte caps
git.status 500 entries max max_entries parameter, capped at 500
git.diff 2MB max max_bytes parameter, capped at 2MB

All caps enforce hard maximums — user-requested values are Math.min(requested, cap).

Timeout Enforcement (repo.run)

  • Default timeout: 120 seconds
  • Hard cap: 300 seconds (5 minutes)
  • SIGTERM sent at timeout, SIGKILL after 2s grace period
  • Timed-out commands return E_TIMEOUT error code

Patch Safety (repo.apply_patch)

  • Pure JS: Uses diff library — no shell patch command
  • Validation pass first: All paths sandbox-checked, all files read, all patches computed before any writes
  • Atomic writes: If any write fails, undo stack rolls back all completed writes
  • No partial application: Either all files patched or none
  • Dry run mode: Validate without writing via dry_run: true

Deterministic Output

  • All tools return ToolEnvelope<T> with ok, data/error, and provenance
  • Array outputs sorted deterministically (lexicographic by path)
  • Timestamps in ISO 8601 format
  • Duration tracked in milliseconds

Audit Logging

  • One JSONL line per tool call: {ts, tool, ok, duration_ms, error_code?, repo_root}
  • Controlled via GHOSTLINK_LOG env var: stdout (default), file, off
  • File mode: writes to logs/ghostlink.jsonl, auto-rotates at 10MB

Future Remote Considerations (Phase 2)

  • OAuth 2.1
  • Tenant separation
  • Rate limiting
  • Structured audit logging (remote transport)

Runtime Considerations (Phase 3)

  • Memory write policies
  • Capability-based authorization

There aren't any published security advisories