Skip to content

security: harden defaults for network-exposed deployments#26

Open
phjlljp wants to merge 1 commit intorepowise-dev:mainfrom
phjlljp:security/harden-defaults
Open

security: harden defaults for network-exposed deployments#26
phjlljp wants to merge 1 commit intorepowise-dev:mainfrom
phjlljp:security/harden-defaults

Conversation

@phjlljp
Copy link
Copy Markdown

@phjlljp phjlljp commented Apr 3, 2026

Summary

Addresses a chain of vulnerabilities that combine into unauthenticated root RCE when repowise is deployed via Docker with default settings (the "Skeleton Key" kill chain: no auth → unrestricted local_path → command injection → root container).

This was identified through a comprehensive security audit with multi-model adversarial validation.

Changes

  • deps.py — Fail-closed authentication when REPOWISE_HOST=0.0.0.0 and no API key is set (returns 503). Uses hmac.compare_digest() for constant-time key comparison. Logs a startup warning when network-exposed without auth. Localhost deployments remain open (no UX change).
  • schemas.py — Pydantic field_validator on RepoCreate.local_path: rejects .. path segments, requires directory to exist and contain .git. Returns resolved absolute path.
  • tool_why.py — Sanitizes stem to [a-zA-Z0-9_\-.] before passing to git log --grep. Adds -- separator to prevent argument injection.
  • Dockerfile — Runs as non-root repowise user. Copies Node.js from the builder stage instead of piping curl | bash (eliminates supply chain risk).
  • docker-compose.yml — Binds ports to 127.0.0.1 (loopback only). Requires REPOWISE_API_KEY and REPO_PATH env vars (docker-compose fails fast if unset). Mounts repos read-only.

Security Model

The fix adopts a dual-mode security model:

  • Local CLI (repowise serve): binds 127.0.0.1, no auth required — unchanged developer experience
  • Docker/network: binds 0.0.0.0, requires API key, non-root container, read-only repo mount

Kill Chains Addressed

Chain Findings Status
Unauthenticated root RCE No auth + path traversal + cmd injection + root Docker Blocked (4 independent mitigations)
Credential theft via browser CORS + plaintext keys Partially mitigated (auth required for network)
Webhook abuse No webhook auth Not addressed (separate PR)

Test plan

  • repowise serve on localhost still works without API key
  • Docker build succeeds and container runs as non-root (docker exec <id> whoamirepowise)
  • docker-compose up fails if REPOWISE_API_KEY or REPO_PATH not set
  • POST /api/repos with local_path=/etc returns 422 validation error
  • POST /api/repos with local_path=/tmp/valid-repo (with .git) succeeds
  • API returns 503 when REPOWISE_HOST=0.0.0.0 and no key set
  • API returns 401 on bad key, 200 on correct key with REPOWISE_API_KEY set

🤖 Generated with Claude Code

Addresses a chain of vulnerabilities that combine into unauthenticated
root RCE when repowise is deployed via Docker with default settings.

Changes:
- deps.py: fail-closed auth when binding 0.0.0.0 without API key,
  use hmac.compare_digest() for constant-time key comparison
- schemas.py: validate local_path in RepoCreate (must be a real git
  repo, no path traversal via '..')
- tool_why.py: sanitize stem passed to git log --grep, add '--'
  separator to prevent argument injection
- Dockerfile: run as non-root user, copy Node.js from builder stage
  instead of piping curl|bash
- docker-compose.yml: bind ports to 127.0.0.1, require REPOWISE_API_KEY
  and REPO_PATH, mount repos read-only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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