GHSA-9mvm-4gwg-v8mp
MEDIUMGHSA-9mvm-4gwg-v8mp is a medium-severity (CVSS 6.3) OS Command Injection vulnerability in github.com/getarcaneapp/arcane/backend. O3 Security confirms whether GHSA-9mvm-4gwg-v8mp is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
Arcane Backend: OS Command Injection in Volume Browser ListDirectory via path query parameter
Exploitation Status
No confirmed exploitation observed yet
- CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.
Exploitation and automatability from CISA’s SSVC triage for GHSA-9mvm-4gwg-v8mp.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
GHSA-9mvm-4gwg-v8mp plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 0 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
github.com/getarcaneapp/arcane/backendReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.
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):
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.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐹Go | github.com/getarcaneapp/arcane/backend | all versions | No fix |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/getarcaneapp/arcane/backend. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Remediation status
No patched version of github.com/getarcaneapp/arcane/backend has shipped for GHSA-9mvm-4gwg-v8mp yet. Where your build allows, override or pin the dependency away from the vulnerable range, and apply any maintainer-recommended mitigation.
Mitigate without a patch
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-9mvm-4gwg-v8mp is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-9mvm-4gwg-v8mp. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-9mvm-4gwg-v8mp in your dependencies?
O3 detects GHSA-9mvm-4gwg-v8mp across Go dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.