# ArgoCD ServerSideDiff is vulnerable to Kubernetes Secret Extraction

ArgoCD ServerSideDiff returns unmasked SSA dry-run output, letting any read-only user extract plaintext Kubernetes Secret data from etcd.

- Payout: Research
- Severity: critical
- Program: ArgoCD
- Found by: hoang-prod
- Disclosed: 2026-05-07
- CVE: CVE-2026-42880
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-3v3m-wc6v-x4x3
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-3v3m-wc6v-x4x3
- Tags: area:cloud domain:cloud domain:web impact:data-leak impact:internal-access kind:research platform:ghsa sev:critical tech:grpc tech:kubernetes technique:source-review vuln:access-control vuln:info-disclosure vuln:secrets-leak

**Target:** ArgoCD ServerSideDiff gRPC/REST endpoint (/application.ApplicationService/ServerSideDiff)
**Root cause:** ServerSideDiff builds its response from raw PredictedLive/NormalizedLive without calling hideSecretData(), and the removeWebhookMutation() defense is skipped when the Application carries argocd.argoproj.io/compare-options: IncludeMutationWebhook=true.
**Steps:**
1. Authenticate with any ArgoCD token having default role:catch-all get access.
2. List secrets via `GET /api/v1/applications/<app>/managed-resources` and collect Secret kind entries with liveState.
3. Build a minimal target Secret manifest omitting `data` (add placeholder keys for kubernetes.io/tls and dockerconfigjson types; copy kubernetes.io/* annotations).
4. POST a grpc-web framed request to `/application.ApplicationService/ServerSideDiff` with Content-Type `application/grpc-web+proto`, X-Grpc-Web: 1, encoding app name, project, live resource and target manifest.
5. Parse response field 5 (targetState) and read `data` values that are not all `+` (masked).
**How found:** Noticed every other endpoint (GetManifests, GetManifestsWithFiles, GetResource, PatchResource) calls hideSecretData() while ServerSideDiff does not, then traced the removeWebhookMutation() guard and found it bypassed by the IncludeMutationWebhook compare-option.
**Exploitation:** Requires the target Application to have annotation argocd.argoproj.io/compare-options: IncludeMutationWebhook=true and the Secret's data fields owned by at least one non-ArgoCD SSA field manager (e.g. kube-controller-manager). The SSA dry-run against the API server returns real etcd values, which flow unmasked into the diff response; the PoC iterates all managed Secrets and prints decoded plaintext keys.
**Escalation:** Extracted service account tokens, TLS certs, DB credentials and API keys enable lateral movement into the cluster and downstream systems.
**Insight:** When auditing diff/compare endpoints, check whether masking helpers are applied uniformly across all handlers; compare-option annotations that disable mutation-webhook stripping can turn SSA dry-run responses into a raw etcd read primitive, but only when a second SSA field manager owns the data fields.

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

**gRPC-web POST to ServerSideDiff endpoint**

```
def grpc_post(conn, token, payload):
    conn.request("POST", "/application.ApplicationService/ServerSideDiff",
        body=grpc_frame(payload), headers={
            "Content-Type": "application/grpc-web+proto",
            "Accept": "application/grpc-web+proto",
            "X-Grpc-Web": "1",
            "Authorization": "Bearer " + token,
        })
    resp = conn.getresponse()
    raw = resp.read()
    if resp.status != 200:
        return None, "HTTP %d" % resp.status
    frames = decode_grpc_frames(raw)
    for flag, fdata in frames:
        if flag == 0:
            return fdata, None
    return None, "no data frame in response"
```

**Unmasked PredictedLive/NormalizedLive response construction**

```
responseDiffs = append(responseDiffs, &v1alpha1.ResourceDiff{
    TargetState:     string(diffRes.PredictedLive),
    LiveState:       string(diffRes.NormalizedLive),
})
```

