Arcane Backend: OS Command Injection in Volume Browser ListDirectory via path query parameter
Description
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.
Details
The execution flow is:
BrowseDirectoryInput.Path(query:path) —backend/internal/huma/handlers/volumes.go:148VolumeHandler.BrowseDirectorycallsvolumeService.ListDirectory(ctx, volumeName, input.Path)—backend/internal/huma/handlers/volumes.go:858-865. Note the route registration at line 412–419 only declaresBearerAuth/ApiKeyAuth; there is nocheckAdmin(ctx)call (compare withcustomize.go,system.go,swarm.go, etc., which do enforce admin).VolumeService.ListDirectoryruns the user-supplied path throughsanitizeBrowsePathInternal, then joins it under/volume, quotes it withstrconv.Quote, and embeds it into ash -ccommand:
// backend/internal/services/volume_service.go:286-300
sanitizedPath, err := s.sanitizeBrowsePathInternal(dirPath)
...
targetPath := path.Join("/volume", sanitizedPath)
quotedPath := strconv.Quote(targetPath)
cmd := []string{"sh", "-c", fmt.Sprintf(
"find %s -mindepth 1 -maxdepth 1 | while IFS= read -r f; do out=$(stat -c \"%%s %%Y %%f %%A\" -- \"$f\" 2>/dev/null) || continue; printf \"%%s\\0%%s\\0\" \"$f\" \"$out\"; done",
quotedPath)}
stdout, _, err := s.execInContainerInternal(ctx, containerID, cmd)
The sanitiser is insufficient (backend/internal/services/volume_service.go:1448-1467):
func (s *VolumeService) sanitizeBrowsePathInternal(input string) (string, error) {
trimmed := strings.TrimSpace(input)
if trimmed == "" || trimmed == "/" { return "/", nil }
cleaned := path.Clean(trimmed)
if !path.IsAbs(cleaned) { cleaned = "/" + cleaned }
if strings.Contains(cleaned, "/../") || strings.HasSuffix(cleaned, "/..") || cleaned == "/.." {
return "", fmt.Errorf("invalid path: path traversal not allowed")
}
if !strings.HasPrefix(cleaned, "/") { return "", fmt.Errorf("invalid path: must be absolute") }
return cleaned, nil
}
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.
Impact
- Authenticated 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 /volumeby sendingpath=.toDELETE /environments/{id}/volumes/{volumeName}/browse, which any authenticated user can also reach.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Authenticated users can inject shell commands via the path parameter in Arcane's volume browser endpoint, leading to arbitrary code execution in the helper container.
Vulnerability
The GET /environments/{id}/volumes/{volumeName}/browse endpoint in Arcane accepts a path query parameter that is passed unsanitized into a shell command (sh -c "find … | while …") inside the helper container [1][2]. The path sanitizer (sanitizeBrowsePathInternal) blocks ../ traversal but does not strip Bourne-shell metacharacters such as $() or backticks. The strconv.Quote function only escapes Go string metacharacters, not shell substitution sequences. This allows any authenticated user with access to a browseable volume to inject arbitrary shell commands. The vulnerable code is in backend/internal/services/volume_service.go lines 286–300 and the sanitizer at lines 1448–1467. All versions of Arcane up to the disclosure date are affected.
Exploitation
An attacker must be an authenticated user with permission to browse a volume (the endpoint only requires BearerAuth/ApiKeyAuth and does not enforce admin privileges). The attacker sends a GET request to /environments/{id}/volumes/{volumeName}/browse?path=$(malicious_command) or using backticks. The injected command is executed inside the helper container via sh -c. The output of the command is reflected in the HTTP 500 error response body, allowing the attacker to read the result [1][2].
Impact
Successful exploitation grants arbitrary command execution inside the Arcane helper container. The attacker can run any shell command, potentially exfiltrating sensitive data, modifying files, or using the container as a pivot point for further attacks within the environment. The compromise is limited to the helper container's privileges, but may lead to broader system compromise depending on container configuration [1][2].
Mitigation
No official patch has been released as of the publication date. Users should restrict network access to the vulnerable endpoint, ensure only trusted users have authentication credentials, and consider disabling volume browsing functionality until a fix is available. The vendor has been notified and a fix is expected in a future release [1][2].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1- Range: <= 1.18.1
Patches
0No patches discovered yet.
Vulnerability mechanics
AI mechanics synthesis has not run for this CVE yet.
References
2News mentions
0No linked articles in our index yet.