GHSA-9mvm-4gwg-v8mp Arcane Backend: OS Command Injection in Volume Browser ListDirectory via path query parameter
Summary`GET /environments/{id}/volumes/{volumeName}/browse` accepts a `path` query parameter that is passed to a shell command (`sh -c "find … | while …"`) inside an Arcane helper container. The path sanitiser blocks `../` traversal but does not strip Bourne-shell metacharacters such as `$()` or backticks, and `strconv.Quote` only escapes Go string metacharacters, not shell substitution sequences. Any authenticated user with access to a browseable volume can execute arbitrary commands inside the helper container; command output is reflected back in the 500 error body.DetailsThe execution flow is:`BrowseDirectoryInput.Path` (query: `path`) — `backend/internal/huma/handlers/volumes.go:148``VolumeHandler.BrowseDirectory` calls `volumeService.ListDirectory(ctx, volumeName, input.Path)` — `backend/internal/huma/handlers/volumes.go:858-865`. Note the route registration at line 412–419 only declares `BearerAuth`/`ApiKeyAuth`; there is no `checkAdmin(ctx)` call (compare with `customize.go`, `system.go`, `swarm.go`, etc., which do enforce admin).`VolumeService.ListDirectory` runs the user-supplied path through `sanitizeBrowsePathInternal`, then joins it under `/volume`, quotes it with `strconv.Quote`, and embeds it into a `sh -c` command:The sanitiser is insufficient (`backend/internal/services/volume_service.go:1448-1467`):Only `../` patterns are filtered. `$(...)`, backticks, `;`, `&`, `|`, `>`, etc. all pass through unchanged. `strconv.Quote` then wraps the path in Go-style double quotes, which `sh -c` interprets as a regular double-quoted string — and bash performs `$(...)` command substitution inside double quotes.For the input `/$( id)`:`sanitizeBrowsePathInternal` returns `/$( id)` (no `../` present).`path.Join("/volume", "/$( id)")` → `/volume/$( id)`.`strconv.Quote(...)` → `"/volume/$( id)"`.The shell runs `find "/volume/$( id)" …`, which expands to `find "/volume/uid=0(root) gid=0(root) groups=0(root)" …`. `find` fails because that path does not exist; the stderr containing the substituted command output is propagated by `execInContainerInternal` (volume_service.go:910-918) into a `command exited with code N: …` error, then re-wrapped by `ListDirectory` and returned to the client as a 500 response body.Errors from the handler at `volumes.go:863-864` are returned via `huma.Error500InternalServerError(err.Error())`, so the substituted output is reflected in plaintext.**Blast radius / mitigations actually present:**The helper container is created by `createTempContainerInternal` with `NetworkDisabled: true`, no privileged mode, no Docker socket mount, only the target Docker volume bind-mounted (`:ro` for browse). It is auto-removed.Therefore the injection executes inside an isolated, network-disabled container that already has read access to the same files the browse API exposes.However: the injection grants arbitrary command execution within that container (well beyond the find/stat/readlink/head primitives the API exposes), enables data exfiltration via error-message side channel, and lets an attacker probe the helper image / volume in ways the legitimate API forbids (e.g. read symlink targets the API explicitly censors at `volume_service.go:336-356`, read past size limits, etc.).A non-admin authenticated Arcane user is sufficient (no role check on the volumes browser routes), which makes this a privilege/capability extension for users who otherwise cannot run arbitrary `docker exec`.**Secondary issue (same sanitiser):** `DeleteFile` (`volume_service.go:924-963`) defends against deleting volume root with `if sanitizedPath == "/"`. Input `path=.` yields `path.Clean(".") == "."` → prefixed to `/.`, which fails the `== "/"` check, then `path.Join("/volume", "/.") == "/volume"`, so the executed command is `rm -rf /volume`, recursively deleting all volume contents. This is a separate logic flaw worth fixing alongside the sanitiser hardening but is reported here only for completeness.ImpactAuthenticated user (any role, including non-admin) can execute arbitrary shell commands inside the per-volume helper container.Output of those commands is reflected in HTTP 500 error bodies — usable as an exfiltration channel.Attacker gains capabilities the legitimate API withholds: bypass the symlink-target censoring at `volume_service.go:336-356`, bypass per-file byte limits, enumerate the helper image, mount-time inspection, etc.No host compromise: the container has `NetworkDisabled: true`, no privileged flag, no Docker socket; the volume is bind-mounted read-only for browse. Confidentiality/integrity/availability impact is therefore limited (CVSS C:L / I:L / A:L) but real.The same insufficient sanitiser additionally permits a destructive `rm -rf /volume` by sending `path=.` to `DELETE /environments/{id}/volumes/{volumeName}/browse`, which any authenticated user can also reach.
CVSS v3.1 score: 6.3