Repository navigation
Description
Severity: security (arbitrary daemon-host file read). Surface: shared remote/proxy daemon HTTP, not the local loopback CLI.
Problem
install_from_source with source.kind: "path" accepts an unconfined filesystem path over the HTTP surface. A remote authenticated caller can name any path the daemon user can read; the daemon materializes it as the install source, so its bytes flow back through the install pipeline. This is an arbitrary host-file read for anyone who can reach /rpc with a valid token.
Evidence (at 848c6ea733)
- The HTTP boundary accepts the path with only a non-empty check:
parseInstallSourceforkind === 'path'—return { kind: 'path', path: artifactPath }, no confinement. - Materialization is unconfined:
materializeLocalSourceforkind: 'path'returnsexpandUserHomePath(source.path)directly (~expansion, then used as-is). - Contrast with the
urlkind, which has real hygiene: credentials rejected, blocked hostnames, DNS-resolved non-public-address rejection, plus redirect caps and HTTPS-downgrade blocks. Thepathkind has none of the equivalent host-side confinement.
Design: kind:"path" is a local affordance; the remote surface must not accept it by default
kind: "path" is legitimate for local/in-process flows and should stay:
admin.tsand its source parse build path sources for local admin.diff-screenshot.tsbuilds one locally.
These do not cross the HTTP parseInstallSource boundary, so gating the HTTP surface leaves them untouched.
Fix:
- Reject
kind: "path"at the HTTP boundary by default. ReturnINVALID_ARGSunless a daemon config explicitly enables host-path installs (e.g.allowHostPathInstall: true), off by default. - When enabled, confine it. Require the resolved, symlink-realpathed absolute path to sit under an operator-configured allowlist root (an install-staging directory), the same "declare the trust boundary" shape the
urlkind already uses for network destinations.
Refactor required
- Gate + optional confinement helper in the HTTP
parseInstallSourcepath branch (src/daemon/server/http-server.ts around L320). Small and self-contained. - Add the daemon config surface for the opt-in + allowlist root; document it as a deliberate expansion of the daemon's trust boundary.
- Local callers (
admin.ts,diff-screenshot.ts) are unaffected and need no change.
Acceptance criteria
- Over
/rpc,install_from_sourcewithkind: "path"is rejected withINVALID_ARGSwhen the opt-in is off (default). Handler-level test. - With the opt-in on and an allowlist root, a path inside the root succeeds and a path outside it (including via
../symlink escape) is rejected. Realpath-based confinement test. - Local flows that build path sources continue to work.
Related
Same "apply the public-URL / trust-boundary rule on the remote surface, leave trusted local flows alone" theme as the Maestro runScript http.post allowlist gap (filed separately).
Effort / risk
M (the gate is small; choosing and wiring the confinement root is the M part), MED risk.
Activity
thymikee commentedon Aug 27, 2026
Remote-daemon hardening cluster (from the correctness & security audit). Suggested pickup order:
All target the shared remote/proxy daemon, not the local loopback CLI. Verified against
848c6ea733.thymikee commentedon Aug 27, 2026
Triage confirmed against current main d44df9b. Ready for an evidence-first agent slice. Keep the first fix narrow: reject source.kind=path unconditionally at the HTTP boundary, while leaving trusted local callers unchanged. Do not add an opt-in host-path compatibility/configuration surface without separate approval. Plant the remote rejection red and retain local-path regression coverage.
fix(daemon): refuse host path install sources through the remote proxy
refactor(proxy): give the host-path refusal its own module
fix(daemon): refuse host path install sources on the HTTP surface (#2102