security: harden defaults for network-exposed deployments#26
security: harden defaults for network-exposed deployments#26phjlljp wants to merge 1 commit intorepowise-dev:mainfrom
Conversation
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>
|
Really appreciate the thoroughness here. Two requests before merge:
Everything else is correct: hmac.compare_digest, non-root Docker user, shlex sanitization in tool_why.py, and the local_path validator. This is important work. |
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 whenREPOWISE_HOST=0.0.0.0and no API key is set (returns 503). Useshmac.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— Pydanticfield_validatoronRepoCreate.local_path: rejects..path segments, requires directory to exist and contain.git. Returns resolved absolute path.tool_why.py— Sanitizesstemto[a-zA-Z0-9_\-.]before passing togit log --grep. Adds--separator to prevent argument injection.Dockerfile— Runs as non-rootrepowiseuser. Copies Node.js from the builder stage instead of pipingcurl | bash(eliminates supply chain risk).docker-compose.yml— Binds ports to127.0.0.1(loopback only). RequiresREPOWISE_API_KEYandREPO_PATHenv vars (docker-compose fails fast if unset). Mounts repos read-only.Security Model
The fix adopts a dual-mode security model:
repowise serve): binds127.0.0.1, no auth required — unchanged developer experience0.0.0.0, requires API key, non-root container, read-only repo mountKill Chains Addressed
Test plan
repowise serveon localhost still works without API keydocker exec <id> whoami→repowise)docker-compose upfails ifREPOWISE_API_KEYorREPO_PATHnot setPOST /api/reposwithlocal_path=/etcreturns 422 validation errorPOST /api/reposwithlocal_path=/tmp/valid-repo(with.git) succeedsREPOWISE_HOST=0.0.0.0and no key setREPOWISE_API_KEYset🤖 Generated with Claude Code