CVE-2026-45626: Arcane Backend OS Command Injection Vulnerability

MODERATE
Advisory Identifier: GHSA-9mvm-4gwg-v8mp
CVSS 6.3
CWE-78
Published
Advisory Summary

CVE-2026-45626 is a MODERATE (CVSS 6.3) os command injection vulnerability (CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')) affecting Arcane Backend libraries in the Go ecosystem. The issue involves os command injection in volume browser listdirectory via path query parameter. Remediation is available in the latest vendor maintenance releases.

CVE-2026-45626 at a Glance

CVE Identifier CVE-2026-45626
GHSA Identifier GHSA-9mvm-4gwg-v8mp
Severity MODERATE
CVSS v3.1 Base Score 6.3
CWE Classification CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
Affected Product Arcane Backend
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 6.3

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality Low (C:L)
Integrity Low (I:L)
Availability Low (A:L)

How to Fix CVE-2026-45626

To remediate this vulnerability, upgrade all affected Arcane Backend dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/getarcaneapp/arcane/backend fixed: Latest secure release
Upgrade github.com/getarcaneapp/arcane/backend to Latest secure release
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/getarcaneapp/arcane/backend

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-45626, inspect your project dependencies for Arcane Backend:

inspect dependencies for github.com/getarcaneapp/arcane/backend

Analyze the terminal output to locate any version of Arcane Backend in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than the patched release), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection'), where 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. In Arcane Backend, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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: ```go // 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`): ```go 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.

CVE-2026-45626 Affected Arcane Backend Versions

1 component(s)
Product / Package Ecosystem Affected Versions Fixed Version
github.com/getarcaneapp/arcane/backend
Go
≤ 1.18.1

References

Frequently Asked Questions

What is CVE-2026-45626?
CVE-2026-45626 is a security vulnerability in Arcane Backend involving OS Command Injection in Volume Browser ListDirectory via path query parameter under CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
Which Arcane Backend versions are affected?
Affected versions include releases prior to current release patches across affected packages: github.com/getarcaneapp/arcane/backend.
What version fixes CVE-2026-45626?
Upgrading to current release patches (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/getarcaneapp/arcane/backend affected?
Yes, github.com/getarcaneapp/arcane/backend is affected in releases prior to Latest secure release. Upgrade your dependency to Latest secure release or later.
What is the CVSS score of CVE-2026-45626?
CVE-2026-45626 has a CVSS v3.1 base score of 6.3 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L.
What CWE is associated with CVE-2026-45626?
CVE-2026-45626 is classified under CWE-78: 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.).
How can I check Go dependencies for CVE-2026-45626?
Run the dependency tree command: 'inspect dependencies for github.com/getarcaneapp/arcane/backend'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-9mvm-4gwg-v8mp",
  "modified" : "2026-06-09T10:30:26Z",
  "published" : "2026-05-18T13:59:22Z",
  "aliases" : [ "CVE-2026-45626" ],
  "summary" : "Arcane Backend: OS Command Injection in Volume Browser ListDirectory via path query parameter",
  "details" : "## Summary\n\n`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.\n\n## Details\n\nThe execution flow is:\n\n1. `BrowseDirectoryInput.Path` (query: `path`) — `backend/internal/huma/handlers/volumes.go:148`\n2. `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).\n3. `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:\n\n```go\n// backend/internal/services/volume_service.go:286-300\nsanitizedPath, err := s.sanitizeBrowsePathInternal(dirPath)\n...\ntargetPath := path.Join(\"/volume\", sanitizedPath)\nquotedPath := strconv.Quote(targetPath)\ncmd := []string{\"sh\", \"-c\", fmt.Sprintf(\n    \"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\",\n    quotedPath)}\nstdout, _, err := s.execInContainerInternal(ctx, containerID, cmd)\n```\n\nThe sanitiser is insufficient (`backend/internal/services/volume_service.go:1448-1467`):\n\n```go\nfunc (s *VolumeService) sanitizeBrowsePathInternal(input string) (string, error) {\n    trimmed := strings.TrimSpace(input)\n    if trimmed == \"\" || trimmed == \"/\" { return \"/\", nil }\n    cleaned := path.Clean(trimmed)\n    if !path.IsAbs(cleaned) { cleaned = \"/\" + cleaned }\n    if strings.Contains(cleaned, \"/../\") || strings.HasSuffix(cleaned, \"/..\") || cleaned == \"/..\" {\n        return \"\", fmt.Errorf(\"invalid path: path traversal not allowed\")\n    }\n    if !strings.HasPrefix(cleaned, \"/\") { return \"\", fmt.Errorf(\"invalid path: must be absolute\") }\n    return cleaned, nil\n}\n```\n\nOnly `../` 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.\n\nFor the input `/$( id)`:\n- `sanitizeBrowsePathInternal` returns `/$( id)` (no `../` present).\n- `path.Join(\"/volume\", \"/$( id)\")` → `/volume/$( id)`.\n- `strconv.Quote(...)` → `\"/volume/$( id)\"`.\n- 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.\n\nErrors from the handler at `volumes.go:863-864` are returned via `huma.Error500InternalServerError(err.Error())`, so the substituted output is reflected in plaintext.\n\n**Blast radius / mitigations actually present:**\n- 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.\n- Therefore the injection executes inside an isolated, network-disabled container that already has read access to the same files the browse API exposes.\n- 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.).\n- 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`.\n\n**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.\n\n## Impact\n\n- Authenticated user (any role, including non-admin) can execute arbitrary shell commands inside the per-volume helper container.\n- Output of those commands is reflected in HTTP 500 error bodies — usable as an exfiltration channel.\n- 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.\n- 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.\n- 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.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/getarcaneapp/arcane/backend"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "last_affected" : "1.18.1"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/getarcaneapp/arcane/security/advisories/GHSA-9mvm-4gwg-v8mp"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-45626"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/getarcaneapp/arcane"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-78" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-18T13:59:22Z",
    "nvd_published_at" : "2026-05-29T18:17:10Z"
  }
}