Skip to content

Arcane Backend: OS Command Injection in Volume Browser ListDirectory via path query parameter

Moderate severity GitHub Reviewed Published May 11, 2026 in getarcaneapp/arcane • Updated Jun 9, 2026

Package

gomod github.com/getarcaneapp/arcane/backend (Go)

Affected versions

<= 1.18.1

Patched versions

None

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:

  1. BrowseDirectoryInput.Path (query: path) — backend/internal/huma/handlers/volumes.go:148
  2. 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).
  3. 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:
// 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 /volume by sending path=. to DELETE /environments/{id}/volumes/{volumeName}/browse, which any authenticated user can also reach.

References

@kmendell kmendell published to getarcaneapp/arcane May 11, 2026
Published to the GitHub Advisory Database May 18, 2026
Reviewed May 18, 2026
Published by the National Vulnerability Database May 29, 2026
Last updated Jun 9, 2026

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
Low
Availability
Low

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L

EPSS score

Exploit Prediction Scoring System (EPSS)

This score estimates the probability of this vulnerability being exploited within the next 30 days. Data provided by FIRST.
(30th percentile)

Weaknesses

Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component. Learn more on MITRE.

CVE ID

CVE-2026-45626

GHSA ID

GHSA-9mvm-4gwg-v8mp

Source code

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.