From be1588606ee0d923357d3e8649de36e88c9ba7cf Mon Sep 17 00:00:00 2001 From: Akash Manna Date: Thu, 18 Dec 2025 21:24:10 +0530 Subject: [PATCH 1/8] Prefer MinGit ssh over system OpenSSH on Windows, keeping OpenSSH as fallback --- .../org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java index 8fe922e6c8..5f31a34d80 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java @@ -2685,6 +2685,15 @@ public File getSSHExecutable() { } } + // Check for ssh.exe on the system PATH as last resort (supports Microsoft OpenSSH and other alternate implementations) + String sshPath = getPathToExe("ssh"); + if (sshPath != null) { + sshexe = new File(sshPath); + if (sshexe.exists()) { + return sshexe; + } + } + throw new RuntimeException( "ssh executable not found. The git plugin only supports official git client https://git-scm.com/download/win"); } From 0323d28f253b1c12354fa8f69f17d01b5d38b4c8 Mon Sep 17 00:00:00 2001 From: Akash Manna Date: Thu, 18 Dec 2025 22:29:58 +0530 Subject: [PATCH 2/8] Enhance SSH command options for improved security by adding BatchMode and disabling PasswordAuthentication --- .../java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java index 5f31a34d80..56aa23ae60 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java @@ -2710,7 +2710,8 @@ Path createWindowsGitSSH(Path key, String user, Path knownHosts) throws IOExcept w.write("setlocal enabledelayedexpansion"); w.newLine(); w.write("\"" + sshexe.getAbsolutePath() - + "\" -i \"!JENKINS_GIT_SSH_KEYFILE!\" -l \"!JENKINS_GIT_SSH_USERNAME!\" " + + "\" -T -i \"!JENKINS_GIT_SSH_KEYFILE!\" -l \"!JENKINS_GIT_SSH_USERNAME!\" " + + "-o BatchMode=yes -o PasswordAuthentication=no " + getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + " %* "); w.newLine(); } @@ -2733,7 +2734,8 @@ Path createUnixGitSSH(Path key, String user, Path knownHosts) throws IOException w.newLine(); w.write("fi"); w.newLine(); - w.write("ssh -i \"$JENKINS_GIT_SSH_KEYFILE\" -l \"$JENKINS_GIT_SSH_USERNAME\" " + w.write("ssh -T -i \"$JENKINS_GIT_SSH_KEYFILE\" -l \"$JENKINS_GIT_SSH_USERNAME\" " + + "-o BatchMode=yes -o PasswordAuthentication=no " + getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + " \"$@\""); w.newLine(); } From 135b8a2085e521cb0a17710ba4213f8a8fa94d52 Mon Sep 17 00:00:00 2001 From: Akash Manna Date: Thu, 18 Dec 2025 22:31:03 +0530 Subject: [PATCH 3/8] apply mvn spotless:apply --- .../java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java index 56aa23ae60..696a7e7109 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java @@ -2685,7 +2685,8 @@ public File getSSHExecutable() { } } - // Check for ssh.exe on the system PATH as last resort (supports Microsoft OpenSSH and other alternate implementations) + // Check for ssh.exe on the system PATH as last resort (supports Microsoft OpenSSH and other alternate + // implementations) String sshPath = getPathToExe("ssh"); if (sshPath != null) { sshexe = new File(sshPath); From 9981442b1180f6dbb9dd697c17fd6fd848d6ce1a Mon Sep 17 00:00:00 2001 From: Akash Manna Date: Fri, 19 Dec 2025 00:14:57 +0530 Subject: [PATCH 4/8] Refactor ssh executable search logic to prioritize system PATH check --- .../plugins/gitclient/CliGitAPIImpl.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java index 696a7e7109..62e27346ce 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java @@ -2626,6 +2626,15 @@ public File getSSHExecutable() { return sshexe; } + // Check for ssh.exe on the system PATH (supports Microsoft OpenSSH and other alternate implementations) + String sshPath = getPathToExe("ssh"); + if (sshPath != null) { + sshexe = new File(sshPath); + if (sshexe.exists()) { + return sshexe; + } + } + // Check Program Files sshexe = getFileFromEnv("ProgramFiles", "\\Git\\bin\\ssh.exe"); if (sshexe != null && sshexe.exists()) { @@ -2685,16 +2694,6 @@ public File getSSHExecutable() { } } - // Check for ssh.exe on the system PATH as last resort (supports Microsoft OpenSSH and other alternate - // implementations) - String sshPath = getPathToExe("ssh"); - if (sshPath != null) { - sshexe = new File(sshPath); - if (sshexe.exists()) { - return sshexe; - } - } - throw new RuntimeException( "ssh executable not found. The git plugin only supports official git client https://git-scm.com/download/win"); } From 9f337917f079acbc98843b1fbf206d859dcb901e Mon Sep 17 00:00:00 2001 From: Akash Manna Date: Sat, 27 Dec 2025 21:01:51 +0530 Subject: [PATCH 5/8] Enhance SSH command options by adding -n flag to prevent reading from stdin --- .../java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java index 62e27346ce..0728c75106 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java @@ -2710,7 +2710,7 @@ Path createWindowsGitSSH(Path key, String user, Path knownHosts) throws IOExcept w.write("setlocal enabledelayedexpansion"); w.newLine(); w.write("\"" + sshexe.getAbsolutePath() - + "\" -T -i \"!JENKINS_GIT_SSH_KEYFILE!\" -l \"!JENKINS_GIT_SSH_USERNAME!\" " + + "\" -n -T -i \"!JENKINS_GIT_SSH_KEYFILE!\" -l \"!JENKINS_GIT_SSH_USERNAME!\" " + "-o BatchMode=yes -o PasswordAuthentication=no " + getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + " %* "); w.newLine(); @@ -2734,7 +2734,7 @@ Path createUnixGitSSH(Path key, String user, Path knownHosts) throws IOException w.newLine(); w.write("fi"); w.newLine(); - w.write("ssh -T -i \"$JENKINS_GIT_SSH_KEYFILE\" -l \"$JENKINS_GIT_SSH_USERNAME\" " + w.write("ssh -n -T -i \"$JENKINS_GIT_SSH_KEYFILE\" -l \"$JENKINS_GIT_SSH_USERNAME\" " + "-o BatchMode=yes -o PasswordAuthentication=no " + getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + " \"$@\""); w.newLine(); From 4ed6db84fff9e7efb612982b9e85d500bae94da4 Mon Sep 17 00:00:00 2001 From: Akash Manna Date: Sat, 27 Dec 2025 21:24:23 +0530 Subject: [PATCH 6/8] Enhance SSH command options by adding StrictHostKeyChecking for improved security --- .../java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java index 0728c75106..4d05524de4 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java @@ -2711,7 +2711,7 @@ Path createWindowsGitSSH(Path key, String user, Path knownHosts) throws IOExcept w.newLine(); w.write("\"" + sshexe.getAbsolutePath() + "\" -n -T -i \"!JENKINS_GIT_SSH_KEYFILE!\" -l \"!JENKINS_GIT_SSH_USERNAME!\" " - + "-o BatchMode=yes -o PasswordAuthentication=no " + + "-o BatchMode=yes -o PasswordAuthentication=no -o StrictHostKeyChecking=yes " + getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + " %* "); w.newLine(); } @@ -2735,7 +2735,7 @@ Path createUnixGitSSH(Path key, String user, Path knownHosts) throws IOException w.write("fi"); w.newLine(); w.write("ssh -n -T -i \"$JENKINS_GIT_SSH_KEYFILE\" -l \"$JENKINS_GIT_SSH_USERNAME\" " - + "-o BatchMode=yes -o PasswordAuthentication=no " + + "-o BatchMode=yes -o PasswordAuthentication=no -o StrictHostKeyChecking=yes " + getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + " \"$@\""); w.newLine(); } From 227f469762ab72c699d0add869fe2d1b2a493fc1 Mon Sep 17 00:00:00 2001 From: Akash Manna Date: Sun, 28 Dec 2025 21:29:27 +0530 Subject: [PATCH 7/8] Remove StrictHostKeyChecking option from SSH command for improved flexibility --- .../java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java index 4d05524de4..0728c75106 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java @@ -2711,7 +2711,7 @@ Path createWindowsGitSSH(Path key, String user, Path knownHosts) throws IOExcept w.newLine(); w.write("\"" + sshexe.getAbsolutePath() + "\" -n -T -i \"!JENKINS_GIT_SSH_KEYFILE!\" -l \"!JENKINS_GIT_SSH_USERNAME!\" " - + "-o BatchMode=yes -o PasswordAuthentication=no -o StrictHostKeyChecking=yes " + + "-o BatchMode=yes -o PasswordAuthentication=no " + getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + " %* "); w.newLine(); } @@ -2735,7 +2735,7 @@ Path createUnixGitSSH(Path key, String user, Path knownHosts) throws IOException w.write("fi"); w.newLine(); w.write("ssh -n -T -i \"$JENKINS_GIT_SSH_KEYFILE\" -l \"$JENKINS_GIT_SSH_USERNAME\" " - + "-o BatchMode=yes -o PasswordAuthentication=no -o StrictHostKeyChecking=yes " + + "-o BatchMode=yes -o PasswordAuthentication=no " + getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + " \"$@\""); w.newLine(); } From d9a3ba4134c38f944b22b802f37e09f968381ee4 Mon Sep 17 00:00:00 2001 From: Akash Manna Date: Sat, 15 Aug 2026 09:45:33 +0530 Subject: [PATCH 8/8] Refactor environment variable handling in CliGitAPIImpl and add tests for SSH executable resolution --- .../plugins/gitclient/CliGitAPIImpl.java | 37 +++--- .../gitclient/CliGitAPISshExecutableTest.java | 125 ++++++++++++++++++ 2 files changed, 147 insertions(+), 15 deletions(-) create mode 100644 src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPISshExecutableTest.java diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java index 420aefc57a..1070faf655 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java @@ -2582,7 +2582,11 @@ private String getPathToExe(String userGitExe) { exe = userGitExe + ".exe"; } - String[] pathDirs = System.getenv("PATH").split(File.pathSeparator); + String path = getEnvVar("PATH"); + if (path == null) { + path = ""; + } + String[] pathDirs = path.split(File.pathSeparator); for (String pathDir : pathDirs) { File exeFile = new File(pathDir, exe); @@ -2603,8 +2607,13 @@ private String getPathToExe(String userGitExe) { return null; } + /* Package protected for testing */ + String getEnvVar(String envVar) { + return System.getenv(envVar); + } + private File getFileFromEnv(String envVar, String suffix) { - String envValue = System.getenv(envVar); + String envValue = getEnvVar(envVar); if (envValue == null) { return null; } @@ -2630,15 +2639,6 @@ public File getSSHExecutable() { return sshexe; } - // Check for ssh.exe on the system PATH (supports Microsoft OpenSSH and other alternate implementations) - String sshPath = getPathToExe("ssh"); - if (sshPath != null) { - sshexe = new File(sshPath); - if (sshexe.exists()) { - return sshexe; - } - } - // Check Program Files sshexe = getFileFromEnv("ProgramFiles", "\\Git\\bin\\ssh.exe"); if (sshexe != null && sshexe.exists()) { @@ -2698,6 +2698,15 @@ public File getSSHExecutable() { } } + // Check the system PATH last, the ssh of the git installation is preferred + String sshPath = getPathToExe("ssh"); + if (sshPath != null) { + sshexe = new File(sshPath); + if (sshexe.exists()) { + return sshexe; + } + } + throw new RuntimeException( "ssh executable not found. The git plugin only supports official git client https://git-scm.com/download/win"); } @@ -2714,8 +2723,7 @@ Path createWindowsGitSSH(Path key, String user, Path knownHosts) throws IOExcept w.write("setlocal enabledelayedexpansion"); w.newLine(); w.write("\"" + sshexe.getAbsolutePath() - + "\" -n -T -i \"!JENKINS_GIT_SSH_KEYFILE!\" -l \"!JENKINS_GIT_SSH_USERNAME!\" " - + "-o BatchMode=yes -o PasswordAuthentication=no " + + "\" -i \"!JENKINS_GIT_SSH_KEYFILE!\" -l \"!JENKINS_GIT_SSH_USERNAME!\" " + getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + " %* "); w.newLine(); } @@ -2738,8 +2746,7 @@ Path createUnixGitSSH(Path key, String user, Path knownHosts) throws IOException w.newLine(); w.write("fi"); w.newLine(); - w.write("ssh -n -T -i \"$JENKINS_GIT_SSH_KEYFILE\" -l \"$JENKINS_GIT_SSH_USERNAME\" " - + "-o BatchMode=yes -o PasswordAuthentication=no " + w.write("ssh -i \"$JENKINS_GIT_SSH_KEYFILE\" -l \"$JENKINS_GIT_SSH_USERNAME\" " + getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + " \"$@\""); w.newLine(); } diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPISshExecutableTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPISshExecutableTest.java new file mode 100644 index 0000000000..4476d18a28 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPISshExecutableTest.java @@ -0,0 +1,125 @@ +package org.jenkinsci.plugins.gitclient; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.jvnet.hudson.test.Issue; + +/** + * Tests the order in which {@link CliGitAPIImpl#getSSHExecutable()} searches for ssh. + * The ssh of the git installation must be preferred over an ssh found on the system PATH. + * + * @author Akash Manna + */ +class CliGitAPISshExecutableTest { + + @TempDir + private File tempDir; + + private CliGitAPIImpl gitClientWithEnv(String gitExe, Map environmentVariables) { + return new CliGitAPIImpl(gitExe, tempDir, null, null) { + @Override + String getEnvVar(String envVar) { + return environmentVariables.get(envVar); + } + }; + } + + /* Paths are joined with a backslash, so on Unix the file name contains backslashes */ + private File createExecutable(File executable) throws IOException { + File parent = executable.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + assertTrue(executable.createNewFile(), "Could not create " + executable); + return executable; + } + + private File pathDirectoryWithSsh() throws IOException { + File pathDir = new File(tempDir, "path-dir"); + pathDir.mkdirs(); + createExecutable(new File(pathDir, "ssh.exe")); + return pathDir; + } + + @Test + @Issue("JENKINS-1715") + void sshOfGitInstallationPreferredOverSshOnPath() throws Exception { + File programFiles = new File(tempDir, "Program Files"); + File gitSsh = createExecutable(new File(programFiles + "\\Git\\bin\\ssh.exe")); + + Map env = new HashMap<>(); + env.put("ProgramFiles", programFiles.getAbsolutePath()); + env.put("PATH", pathDirectoryWithSsh().getAbsolutePath()); + + assertEquals(gitSsh, gitClientWithEnv("git", env).getSSHExecutable()); + } + + @Test + @Issue("JENKINS-1715") + void sshOfGitInstallationInUsrBinPreferredOverSshOnPath() throws Exception { + File programFiles = new File(tempDir, "Program Files"); + File gitSsh = createExecutable(new File(programFiles + "\\Git\\usr\\bin\\ssh.exe")); + + Map env = new HashMap<>(); + env.put("ProgramFiles", programFiles.getAbsolutePath()); + env.put("PATH", pathDirectoryWithSsh().getAbsolutePath()); + + assertEquals(gitSsh, gitClientWithEnv("git", env).getSSHExecutable()); + } + + @Test + @Issue("JENKINS-1715") + void sshBesideGitExecutablePreferredOverSshOnPath() throws Exception { + File gitExe = new File(tempDir, "MinGit\\cmd\\git.exe"); + File gitSsh = createExecutable(new File(gitExe.getParent() + "\\ssh.exe")); + + Map env = new HashMap<>(); + env.put("PATH", pathDirectoryWithSsh().getAbsolutePath()); + + assertEquals(gitSsh, gitClientWithEnv(gitExe.getAbsolutePath(), env).getSSHExecutable()); + } + + @Test + @Issue("JENKINS-72450") + void sshOnPathUsedWhenGitInstallationHasNoSsh() throws Exception { + File pathDir = pathDirectoryWithSsh(); + + Map env = new HashMap<>(); + env.put("PATH", pathDir.getAbsolutePath()); + + assertEquals( + new File(pathDir, "ssh.exe"), + gitClientWithEnv(new File(tempDir, "no-ssh-here\\git.exe").getAbsolutePath(), env) + .getSSHExecutable()); + } + + @Test + @Issue("JENKINS-72450") + void gitSshEnvironmentVariablePreferredOverSshOnPath() throws Exception { + File gitSshVariable = createExecutable(new File(tempDir, "custom-ssh.exe")); + + Map env = new HashMap<>(); + env.put("GIT_SSH", gitSshVariable.getAbsolutePath()); + env.put("PATH", pathDirectoryWithSsh().getAbsolutePath()); + + assertEquals(gitSshVariable, gitClientWithEnv("git", env).getSSHExecutable()); + } + + @Test + void sshNotFoundReportsUnsupportedGitInstallation() { + Map env = new HashMap<>(); + env.put("PATH", new File(tempDir, "empty-dir").getAbsolutePath()); + + CliGitAPIImpl git = gitClientWithEnv(new File(tempDir, "no-ssh-here\\git.exe").getAbsolutePath(), env); + RuntimeException e = assertThrows(RuntimeException.class, git::getSSHExecutable); + assertTrue(e.getMessage().startsWith("ssh executable not found"), e.getMessage()); + } +}