Commit Graph

7 Commits

Author SHA1 Message Date
Anso bee0dfd094 fix: contain file-explorer binds that reach dangling symlinked managed paths (#1490)
A previous fix canonicalized each Sencho-managed path (compose base, data dir,
application root, OS temp root, upload spool, Trivy binary, Trivy cache) with
fs.realpath so a managed path that is itself a symlink could not be reached through
a bind to its real target. fs.realpath returns ENOENT for a dangling symlink whose
final leaf does not exist yet, so that case fell through to the configured path only
and the implied target was lost. With TRIVY_BIN pointing at a symlink to a not yet
created binary, a bind to the symlink target's existing parent directory was still
classified as browsable and writable. A stack editor could create the leaf there
and have a later pre or post deploy scan execute the attacker supplied binary, which
the default image runs as root. The same gap applied to dangling symlinked upload
spool, Trivy cache, and OS temp roots.

Resolve managed paths with a helper that follows the symlink chain even when the
final leaf is absent: it follows a dangling link through readlink (resolving a
relative target against the link's own directory) and, for a missing leaf, resolves
the longest existing ancestor and re-appends the absent suffix, so the real target
location is always represented in the overlap set. A bounded hop count guards
against a symlink cycle that never surfaces as ELOOP, falling back to the lexical
path with a log. An unexpected resolution failure (EACCES, ELOOP) keeps the
configured path as the containment anchor, continues discovery for the other
managed paths, and is logged.

Tests: a mock-based regression test that fails before the fix, a real-filesystem
dangling-symlink test on Linux and macOS CI, a relative-symlink-target case, and a
non-ENOENT failure case asserting discovery degrades safely.
2026-06-27 14:32:50 -04:00
Anso 349ee1f905 fix: contain file-explorer binds that reach symlinked Sencho-managed paths (#1489)
The file-explorer overlap check canonicalized declared bind sources (it realpaths
the source) but compared them against the Sencho-managed paths (compose base, data
dir, application root, OS temp root, upload spool, Trivy binary, Trivy cache) using
path.resolve only. A managed path that is itself a symlink, for example a relocated
Trivy binary whose configured path links to a real binary elsewhere, was therefore
compared by its symlink path. A bind to the symlink target's real directory did not
register as a managed overlap and became browsable and writable, so a stack editor
could overwrite the real binary a later pre or post deploy scan executes, or reach
transient registry credentials under a symlinked temporary root.

Resolve each managed path to its canonical target and keep both the configured path
and the realpath target in the overlap set, mirroring the bind-side canonicalization.
Containment only ever expands, so existing deployments and fresh installs are
unaffected. A missing managed path (the common fresh-install case) is tolerated: the
configured path still anchors containment, and any non-ENOENT realpath failure is
logged rather than collapsing discovery.

Adds two tests: a regression test that fails when a symlinked managed Trivy path is
compared without canonicalization, and a fresh-install test that an absent managed
path leaves a legitimate external bind browsable.
2026-06-27 00:43:29 -04:00
Anso 3386c63152 fix: contain file-explorer binds into Sencho's temporary and tool directories (#1487)
The file-explorer root containment treated the OS temp root as an ordinary
host path, so a stack author with stack:edit could declare it (for example
/tmp) as a bind source and browse it. Sencho writes short-lived secrets there:
ComposeService and TrivyService stage a docker config.json holding resolved
registry credentials, uploads spool under it, and compose/git/scan runs create
working dirs there. Exposing that directory is a credible path to read
admin-configured registry credentials during a pull or deploy.

The same gap left env-relocatable tool paths outside containment: a Trivy
binary placed at a custom TRIVY_BIN (for example under /opt, which is otherwise
allowed) could be overwritten through a bind and then executed by a privileged
pre-deploy scan.

Treat the OS temp root and the configurable upload spool, Trivy binary, and
Trivy cache as Sencho-managed areas, so a bind overlapping any of them (in
either direction) is never browsable, writable, or chmodable. The managed Trivy
install and cache already sit under the data dir and stay covered. Legitimate
external binds outside these areas remain fully editable.
2026-06-26 23:26:46 -04:00
Anso 73f4bc27c3 fix: contain file-explorer binds into Sencho's system directories (#1484)
The file-explorer root containment treated only kernel and OS-state paths
(/etc, /proc, /sys, /dev, /run, /var/run) as dangerous. System locations
that hold the executables and libraries Sencho's own runtime depends on,
notably /usr (which contains /usr/local/bin/node, the docker CLI, and the
entrypoint) plus /bin, /sbin, /lib, /lib64, /boot and /root, were left
browsable, writable and chmodable.

A stack author with stack:edit could declare one of these as a bind source,
overwrite a binary, and have a later deploy execute it. Add those locations
to the dangerous-root set so such a bind is never browsable or editable; the
boundary check still permits ordinary host paths whose name merely prefixes a
protected root (for example /usrdata).
2026-06-26 22:39:33 -04:00
Anso 3e2f0450c2 fix: block file-explorer binds that overlap Sencho's application directory (#1479)
The Files & Volumes explorer derives its browsable roots from a stack's declared
bind mounts. A bind whose source resolves inside Sencho's own application root
(the working directory the image runs from, holding the compiled dist/, the
served public/, and node_modules) was classified accessible, browsable, and
writable. A non-admin with stack edit rights could therefore declare a bind such
as /app/dist into a stack and gain read/write access to Sencho's program files.

The bind-root classifier now treats the application root as a managed area, the
same way it already treats the compose base and the data directory, so a bind
that overlaps it is non-browsable and non-writable and the file routes reject
read and write operations against it. The check is gated on the bind not being
inside the current stack directory, so a legitimate stack-scoped bind under the
compose base (which can sit under the application root) stays browsable.

The application root is resolved dynamically from the process working directory,
mirroring how the data directory is resolved, rather than hardcoding a path.
2026-06-26 21:11:37 -04:00
Anso 1c82e3e1d4 fix: contain file-explorer writes and browse reachable out-of-base binds (#1465)
The file-explorer editor save resolved a dangling symlink leaf to the link
path and wrote through it with a plain writeFile, which followed the link and
created a file outside the bind/stack root. Reject a resolved leaf that is
itself a symlink (mirroring the managed-stack guard) and promote the save
through the atomic stage-and-rename helper, so the editor save matches its
documented atomicity and can never leave a partial file or land outside the
root.

Bind-root discovery reported every source outside the compose base as
unreachable without probing it, so a config directory mounted into both the
app and the Sencho container was wrongly non-browsable. Probe the declared
source as Sencho actually sees it; dangerous host roots, docker-socket mounts,
and managed-area overlaps stay blocked, and the dangerous classification also
reads the literal declared source so it holds across platforms.
2026-06-26 09:23:50 -04:00
Anso b9d8e9f490 feat(stacks): browse and edit mounted volume files in the explorer (#1403)
* feat(stacks): browse and edit mounted volume files in the explorer

Reposition the stack file explorer around runtime configuration access:
discover a stack's declared mounts and expose each as a safe, stack-scoped
file root. The explorer opens on a Volumes group (bind mounts and named
Docker volumes) by default, with the stack source directory as a secondary
group, on a "Files & Volumes" tab.

- Discover roots from the rendered effective compose model; resolve named
  volumes to their Docker name and browse/edit them through the hardened
  helper container, with bind mounts handled directly when reachable.
- Re-derive the allowed roots server-side on every file operation and match
  the client root id against them, so a request can never address a path the
  stack did not declare. Block dangerous host mounts and binds that overlap
  Sencho's managed directories; reject writes to read-only mounts.
- Thread an optional root id through the existing file endpoints and an
  opaque, parseable optimistic-concurrency token through read, conflict,
  and write, for both filesystem and helper backends.
- Keep compose and env file protection on the stack source root only.

* fix(stacks): theme the Files & Volumes root switcher

Replace the raw native select in the file-root switcher with the design
system Select component. The native control did not honour the dark theme,
so the panel rendered white with unreadable text. The themed Select gives a
dark popover with grouped Volumes / Stack source labels and disabled items.

* fix(stacks): contain the bind-root probe and de-taint the file-op error log

Gate the volume-root bind probe's realpath/stat behind a compose-base
containment check (mirroring the storage host-path probe) so they never run
on an unvalidated host path; a source outside the compose dir is unreachable
in the containerized deployment anyway and is reported non-accessible without
touching the filesystem. Log the helper-backed file-op failure through a
constant format string with sanitized arguments instead of an interpolated
template literal.

* fix(stacks): inline the bind-probe containment guard at the fs sinks

The wrapped containment predicate was not recognized as a path barrier, so
the bind probe's realpath/stat still flagged as uncontrolled-data-in-path.
Inline the path.resolve + startsWith check directly at each filesystem sink
(and re-check the resolved canonical before stat, so a within-base symlink
that resolves outside the compose dir is also rejected).

* fix(stacks): harden file-root lifecycle, upload race, and helper errors

Address review findings on the Files & Volumes feature:

- Invalidate the file-root allowlist on stack create/delete/import/from-git
  (wire StackFileRootsService.invalidateNode into invalidateNodeCaches), so a
  stack deleted and recreated under the same name cannot serve the old stack's
  roots from the 15s cache.
- Use the atomic exclusive write for a non-overwrite upload so a file created
  by another writer after the existence check is not silently clobbered.
- Let the helper's real cd errno through and map permission failures to 403
  consistently across list/stat/read/write/mkdir/delete/pathKind, instead of
  reporting EACCES as 404/500; pathKind no longer reports a permission-denied
  parent as absent.
- Document the realpath-then-open TOCTOU as a known, pre-existing limitation of
  every file op (O_NOFOLLOW is not viable because config volumes legitimately
  contain symlinks); the bind root is contained to the compose dir and the op
  requires stack:edit.
- Docs: drop a missing screenshot reference and correct the protected-file
  delete behavior (stack-root compose/.env cannot be deleted via the explorer).
2026-06-21 18:16:20 -04:00