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):
sanitizeBrowsePathInternalreturns/$( id)(no../present).path.Join("/volume", "/$( id)")→/volume/$( id).strconv.Quote(...)→"/volume/$( id)".- The shell runs
find "/volume/$( id)" …, which expands tofind "/volume/uid=0(root) gid=0(root) groups=0(root)" ….findfails because that path does not exist; the stderr containing the substituted command output is propagated byexecInContainerInternal(volume_service.go:910-918) into acommand exited with code N: …error, then re-wrapped byListDirectoryand 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
createTempContainerInternalwithNetworkDisabled: true, no privileged mode, no Docker socket mount, only the target Docker volume bind-mounted (:rofor 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.
Untrusted input reaches a shell command, allowing arbitrary commands to run on the host. Typical impact: code execution in the application's environment.
CVE-2026-45626 has a CVSS score of 6.3 (Medium). The vector is network-reachable, low privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. No fixed version is listed yet, so configuration controls and monitoring matter more in the interim.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Remediation advice
In the interim: Avoid passing untrusted input to shell commands. Use parameterized APIs or libraries that do not invoke a shell.
Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.
Frequently Asked Questions
- What is CVE-2026-45626? CVE-2026-45626 is a medium-severity OS command injection vulnerability in github.com/getarcaneapp/arcane/backend (go), affecting versions <= 1.18.1. No fixed version is listed yet. Untrusted input reaches a shell command, allowing arbitrary commands to run on the host.
- How severe is CVE-2026-45626? CVE-2026-45626 has a CVSS score of 6.3 (Medium). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which versions of github.com/getarcaneapp/arcane/backend are affected by CVE-2026-45626? github.com/getarcaneapp/arcane/backend (go) versions <= 1.18.1 is affected.
- Is there a fix for CVE-2026-45626? No fixed version is listed for CVE-2026-45626 yet. Monitor the advisory for updates and apply mitigations in the interim.
- Is CVE-2026-45626 exploitable, and should I be worried? Whether CVE-2026-45626 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2026-45626 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2026-45626? No fixed version is listed yet. In the interim: Avoid passing untrusted input to shell commands. Use parameterized APIs or libraries that do not invoke a shell.