Severity by source
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L
Primary rating from GitHub Advisory · only source for this CVE.
CVSS VectorGitHub Advisory
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L
Lifecycle Timeline
2DescriptionGitHub Advisory
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.
AnalysisAI
OS command injection in the Arcane backend volume browser endpoint (all versions ≤ 1.18.1) allows any authenticated user - including non-admin roles - to execute arbitrary shell commands inside the per-volume helper container by supplying Bourne shell metacharacters such as $() in the path query parameter of GET /environments/{id}/volumes/{volumeName}/browse. The path sanitizer at volume_service.go:1448-1467 blocks only ../ traversal and passes shell substitution sequences through unchanged; strconv.Quote wraps the path in Go-style double quotes, which POSIX sh still interprets as a command-substitutable string, causing the injected command to execute and its output to be reflected in the HTTP 500 error body. No vendor-released patch exists at time of analysis; publicly available exploit code is embedded in the GHSA advisory (GHSA-9mvm-4gwg-v8mp) and no confirmed active exploitation (CISA KEV) has been reported.
Technical ContextAI
The affected package is pkg:go/github.com/getarcaneapp/arcane/backend (CPE: pkg:go/github.com_getarcaneapp_arcane_backend), all versions ≤ 1.18.1. The root cause maps to CWE-78 (Improper Neutralization of Special Elements Used in an OS Command) arising from two compounding mistakes in the Go backend. First, sanitizeBrowsePathInternal (volume_service.go:1448-1467) performs path-traversal filtering only - it removes ../ sequences but permits all Bourne shell metacharacters including $(), backticks, ;, &, |, and >. Second, strconv.Quote is misapplied: it escapes Go string metacharacters and wraps the result in double quotes, but does not sanitize shell substitution syntax - POSIX sh performs $(...) expansion inside double-quoted strings. The resulting path is interpolated directly into a sh -c format string via fmt.Sprintf at volume_service.go:286-300, and the child process is spawned inside an ephemeral Docker helper container via execInContainerInternal. When the injected command causes find to fail because the expanded path does not exist as a directory, the error output - containing command results - is propagated through volumes.go:863-864 as a huma.Error500InternalServerError and returned to the caller in plaintext. The route at volumes.go:412-419 enforces only BearerAuth/ApiKeyAuth and deliberately omits the checkAdmin(ctx) guard present on other sensitive routes such as customize.go, system.go, and swarm.go, expanding the potential attacker population to all authenticated users.
RemediationAI
No vendor-released patch has been identified at time of analysis - the advisory at https://github.com/getarcaneapp/arcane/security/advisories/GHSA-9mvm-4gwg-v8mp records 'fixed in: None' for all versions ≤ 1.18.1. Monitor the advisory and the upstream repository at https://github.com/getarcaneapp/arcane for a patched release. Until a patch is available, the highest-value compensating control is to restrict GET and DELETE requests to /environments/{id}/volumes/{volumeName}/browse at the reverse proxy or API gateway layer so that only admin-role users can reach these routes, mirroring the checkAdmin(ctx) pattern already enforced on other sensitive Arcane routes - this eliminates non-admin exploitation but does not fully remediate the injection for admin users. If the volume browse feature is not operationally required, block these routes entirely at the network perimeter or load balancer. As a detection measure, monitor HTTP 500 responses from the Arcane backend for anomalous content patterns such as shell output strings (e.g., uid/gid strings, directory listings) in response bodies, which would indicate active exploitation via the error-reflection channel. Note that the container-level isolation (NetworkDisabled: true, read-only volume bind mount) already limits exfiltration to the error-message side channel and prevents direct host compromise, but does not prevent the injection itself or the secondary rm -rf /volume destructive path accessible via DELETE /environments/{id}/volumes/{volumeName}/browse?path=..
An issue was discovered in Appsmith before 1.52. Rated critical severity (CVSS 9.8), this vulnerability is remotely expl
runc through version 1.0-rc6 (used in Docker before 18.09.2) contains a container escape vulnerability that allows attac
Netmaker makes networks with WireGuard. Rated high severity (CVSS 7.5), this vulnerability is remotely exploitable, no a
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
The News & Blog Designer Pack - WordPress Blog Plugin - (Blog Post Grid, Blog Post Slider, Blog Post Carousel, Blog Post
Docker 1.3.2 allows remote attackers to execute arbitrary code with root privileges via a crafted (1) image or (2) build
Remote code execution in NocoBase Workflow Script Node (npm @nocobase/plugin-workflow-javascript) allows authenticated l
Docker Desktop Community Edition before 2.1.0.1 allows local users to gain privileges by placing a Trojan horse docker-c
Vasion Print (formerly PrinterLogic) Virtual Appliance Host prior to version 25.2.169 and Application prior to version 2
An issue in Plone Docker Official Image 5.2.13 (5221) open-source software that could allow for remote code execution du
Tandoor Recipes is an application for managing recipes, planning meals, and building shopping lists. Rated critical seve
Unauthenticated remote code execution in 9router (npm package) versions 0.4.30 through 0.4.36 allows network-adjacent at
Same weakness CWE-78 – OS Command Injection
View allSame technique Path Traversal
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-33372
GHSA-9mvm-4gwg-v8mp