Safeguard
Security•

Argo CD CVE-2026-55797: A Proxy URL That Runs Commands in the Repo-Server

Anyone who can add or edit an Argo CD repository could run shell commands in the repo-server through an SSH proxy URL. Affected versions, the root cause, what the fix changed, and a script to find risky repository Secrets today.

Safeguard Security Team
9 min read

If someone can add or edit a repository in Argo CD, they can run shell commands inside the repo-server. CVE-2026-55797 is an OS command injection in how Argo CD builds the SSH command for Git repositories that sit behind a proxy: the proxy hostname is pasted into a shell string, and a hostname with the wrong characters ends that string early.

The repo-server is a bad place to lose. It is the component that holds your Git, Helm and OCI credentials so it can render manifests. This post is for platform teams running Argo CD. It covers which versions are affected, why the bug existed, what the fix changed, and a script that finds risky repository Secrets in a cluster today.

The short answer

CVE-2026-55797 (GHSA-j6cw-g6p4-7hch, CVSS 8.8) affects Argo CD v2.11.0 and later, and is fixed in 3.3.15, 3.4.10, 3.5.4 and 3.6.0-rc2. It triggers only for SSH Git repositories that have a proxy URL set. Anyone allowed to create or update a repository or a repository credential template can set that proxy, including users with project-scoped repository permissions. Argo CD 2.x and 3.0 through 3.2 are out of support and will not be patched, so the fix for those lines is an upgrade to 3.3.15 or newer. GitHub's advisory credits seven reporters: kamil-sawicki, yuvalelarat, cherez0ff, McCaulay, ria-labs-security, parameter-ai-security and zwindler.

Release lineAffectedFixed in
2.10.x and earlierNon/a
2.11.0 to 2.14.xYesNo patch, upgrade to 3.3.15+
3.0.x to 3.2.xYesNo patch, upgrade to 3.3.15+
3.3.xBelow 3.3.153.3.15
3.4.xBelow 3.4.103.4.10
3.5.xBelow 3.5.43.5.4
3.6.0-rc1Yes3.6.0-rc2

The patched releases went out on 6 October 2026. The advisory reached the GitHub Advisory Database on 9 October.

Where the shell comes from

Argo CD added SOCKS5 proxy support for SSH Git URLs in January 2024 (commit 9b27aeb1a4, PR #15864), first released in v2.11.0. Git does not speak to proxies over SSH by itself, so Argo CD tells SSH to reach the server through a helper program using a ProxyCommand.

Here is the relevant code as it stood in v3.3.14, in util/git/creds.go:

args = append(args, "-o", fmt.Sprintf("ProxyCommand='connect-proxy -S %s:%s -5 %%h %%p'",
    parsedProxyURL.Hostname(),
    parsedProxyURL.Port()))
// ...
env = append(env, []string{"GIT_SSH_COMMAND=" + strings.Join(args, " ")}...)

Two things make this dangerous. First, GIT_SSH_COMMAND is not an argument vector. Git runs it through a shell, so quoting and metacharacters inside it are interpreted. Second, the proxy host is wrapped in single quotes, and nothing stops the host itself from containing a single quote. A hostname that closes the quote can append its own shell syntax, and the repo-server runs it when it next connects to the repository.

The advisory notes that http, https and socks5 proxy URLs all take this path when set on an SSH repository. An HTTPS Git repository uses the proxy as a normal HTTP proxy and never builds this command.

"It parsed as a URL" is not a security check

The code did call url.Parse on the proxy first, which looks like validation. It isn't, for this purpose. RFC 3986 allows "sub-delimiter" characters in a host, and Go's parser follows the RFC. You can check which characters survive with a few lines of Go:

// hostchars.go  (go run hostchars.go)
package main

import (
	"fmt"
	"net/url"
)

func main() {
	for _, s := range []string{
		"socks5://a'b:1080", "socks5://a;b:1080", "socks5://a$b:1080", "socks5://a(b):1080",
		"socks5://a b:1080", "socks5://a`b:1080", "socks5://a|b:1080",
	} {
		u, err := url.Parse(s)
		if err != nil {
			fmt.Printf("%-22q rejected\n", s)
			continue
		}
		fmt.Printf("%-22q host=%q\n", s, u.Hostname())
	}
}

On Go 1.27.1 the output is:

"socks5://a'b:1080"    host="a'b"
"socks5://a;b:1080"    host="a;b"
"socks5://a$b:1080"    host="a$b"
"socks5://a(b):1080"   host="a(b)"
"socks5://a b:1080"    rejected
"socks5://a`b:1080"    rejected
"socks5://a|b:1080"    rejected

Spaces, backticks and pipes are refused. Single quotes, semicolons, dollar signs and parentheses are not. A URL parser answers "is this a well-formed URL?" The question the code needed answered was "is this safe to put inside a shell string?", and those are different questions with different answers.

Who can trigger it

This is not an unauthenticated bug, which is why the CVSS vector says PR:L. The advisory lists the ways to set a proxy:

  • the Argo CD RBAC repositories resource with create or update, including project-scoped repository permissions
  • argocd repo add --proxy
  • writing a proxy field directly into a repository Secret or a credential template (repo-creds) Secret

Credential templates widen the blast radius. A template's proxy applies to every matching SSH repository that has no credentials of its own, so one edited template can affect many repositories. The command runs when Argo CD tests the repository connection and again on later fetches. Per the advisory, the process can read the other Git, Helm and OCI credentials stored for the repo-server.

In plenty of organisations, "can add a repository to their own project" is a permission handed to every application team. For those setups, this bug turns a routine self-service permission into code execution inside a shared, credential-holding component.

What the fix changed

The fix (commit 1e3ddd0b) does two independent things, and both are worth copying.

It validates the proxy against an allowlist, not a parser. A new validateProxyURL accepts only socks5 and socks5h schemes for SSH credentials. It requires the host to match ^[a-zA-Z0-9.\-]+$, or to parse as an IPv6 address with net.ParseIP. The port must be numeric and between 1 and 65535. When a stored SSH credential fails this check, Argo CD drops the proxy and logs a security warning tagged CWE-78, instead of quietly using it.

It stops putting the host in the shell string at all. The ProxyCommand now points to a small wrapper script written to a temporary file with mode 0700. The GIT_SSH_COMMAND string carries only the script path. The comment in the commit states the intent directly: the hostname and port should no longer be embedded in the shell-interpreted string.

There is a compatibility consequence. If you were using an http or https proxy on an SSH repository, the new SSH validation only accepts SOCKS5 schemes, so check that those repositories still connect after you upgrade.

Check your cluster today

Start with the version you are actually running:

kubectl -n argocd get deploy argocd-repo-server \
  -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'

Then find every repository or credential template Secret that combines an SSH URL with a proxy. Save this filter as audit.jq:

.items[]
| select(.data.proxy != null)
| { name: .metadata.name,
    type: .metadata.labels["argocd.argoproj.io/secret-type"],
    url:  (.data.url   | @base64d),
    proxy:(.data.proxy | @base64d) }
| select(.url | test("^ssh://|^[^/:@]+@[^:]+:"))
| .host = (.proxy | sub("^[a-z0-9]+://"; "") | sub("^[^@]*@"; "") | sub(":[0-9]*/?$"; ""))
| .verdict = (if (.host | test("^[A-Za-z0-9.-]+$")) then "SSH+proxy: patch or remove proxy"
              else "UNSAFE HOST: investigate now" end)
| "\(.verdict)\t\(.type)/\(.name)\t\(.url)\t\(.proxy)"

And run it:

kubectl -n argocd get secrets \
  -l 'argocd.argoproj.io/secret-type in (repository,repo-creds)' -o json \
  | jq -r -f audit.jq

Any UNSAFE HOST line means a proxy value contains characters a hostname never needs. Treat that as a possible compromise of the repo-server, not a config typo: rotate the credentials it could reach and review the repo-server's logs and activity. SSH+proxy lines are not malicious, but they are exactly the configuration this CVE exploits, so they are the ones to fix first. (The output prints proxy URLs, which can include SOCKS credentials, so run it somewhere private.)

Finally, see who can create or update repositories. For the global policy:

kubectl -n argocd get cm argocd-rbac-cm -o jsonpath='{.data.policy\.csv}' \
  | grep -E '^\s*p,\s*[^,]+,\s*repositories,\s*(create|update|\*)\s*,'

Project roles live in each AppProject, so check those too:

kubectl -n argocd get appprojects -o json \
  | jq -r '.items[] | .metadata.name as $p | .spec.roles[]? | .policies[]?
           | select(test("repositories,\\s*(create|update|\\*)")) | "\($p)\t\(.)"'

If you cannot upgrade this week

The advisory is blunt that no setting keeps SSH proxies while removing the shell command. The interim options are about who can reach the feature:

  1. Remove create and update on repositories from anyone you would not trust with a shell in the repo-server, including project-scoped grants.
  2. Do not set a proxy on SSH repositories or on credential templates that match SSH URLs. If you need a proxy, an HTTPS repository with an HTTP proxy does not use this code path.
  3. Restrict who can write Secrets in the argocd namespace. Kubernetes RBAC on Secrets is a second route to the same proxy field, and Argo CD's own RBAC does not cover it.

For anyone still on 2.x, this CVE is a hard deadline. There will not be a 2.14 patch.

How bad is this really?

It is fair to push back on the urgency. The attacker needs an authenticated Argo CD permission or write access to Secrets in the Argo CD namespace, and the target repository has to use SSH with a proxy. In a small team where only platform engineers touch repositories, the realistic risk is low.

It becomes serious in multi-tenant setups, which is how Argo CD is often run. Project-scoped repositories exist so application teams can onboard themselves. A permission meant for "add my team's repo" was, until 6 October, enough to execute code next to every other team's credentials. Our Argo CD GitOps security guide and the post on hardening the Argo CD to Kubernetes pipeline cover how to separate those tenants more firmly.

The pattern to take away

Any time a program builds a string that a shell will interpret, every value inside it must be checked against what that value is allowed to contain, not merely against whether it parses. Argo CD's fix does the strong version of this: an allowlist for the host, and a design that keeps the host out of the shell string entirely. The second part matters more. Validation can be loosened by a later refactor. A value that never reaches the shell cannot be used to inject into it.

It is also a reminder that the version of platform tooling you deploy drifts from the one people believe they deployed. Scanning the container images themselves, not just the charts in Git, is part of what Safeguard ESSCM does.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.