CVE-2022-36881
Description
Jenkins Git client Plugin 3.11.0 and earlier omits SSH host key verification, enabling man-in-the-middle attacks.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Jenkins Git client Plugin 3.11.0 and earlier omits SSH host key verification, enabling man-in-the-middle attacks.
Vulnerability
Overview
The Jenkins Git client Plugin, versions 3.11.0 and earlier, fails to verify SSH host keys when establishing connections to Git repositories over SSH. This omission means the plugin does not confirm the identity of the remote server, leaving the connection unauthenticated at the transport layer [1][4].
Exploitation
Prerequisites
An attacker with network access between a Jenkins controller/agent and a Git repository can exploit this weakness by intercepting the SSH connection. No prior authentication to Jenkins is required for the man-in-the-middle attack, as the vulnerability lies purely in the transport-layer handling of the plugin [1][2].
Impact
Successful exploitation allows an attacker to impersonate the legitimate Git repository, potentially injecting malicious code or stealing credentials transmitted during the session. This undermines the integrity of software builds and can lead to supply-chain compromise [1][4].
Mitigation
Git client Plugin version 3.11.1 resolves the issue by introducing configurable SSH host key verification strategies, enabling administrators to enforce verification that matches their security policies [1][2]. Users should upgrade immediately.
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.jenkins-ci.plugins:git-clientMaven | < 3.11.1 | 3.11.1 |
Affected products
2- Range: unspecified
Patches
188f52c6c9b18[SECURITY-1468]
33 files changed · +1277 −14
images/ssh-host-key-verification.png+0 −0 addedREADME.adoc+10 −0 modified@@ -106,6 +106,16 @@ Windows Credentials Manager works very well for interactive users on the Windows Windows Credentials Manager does not work as well for batch processing in the git client plugin. It is best to disable Windows Credentials Manager when installing Git on Jenkins agents running Windows. +[#ssh-host-key-verification] +== Ssh Host Key verification + +Git Client plugin provides various options to verify the SSH keys presented by Git repository host servers. +By default, Git Client plugin uses "Accept first connection" strategy, which automatically adds keys for hosts that have not been seen before to `known_hosts`, and does not allow connections to previously-seen hosts with modified keys. +Configure the host key verification strategy from "Manage Jenkins" >> "Configure Global Security" >> "Git Host Key Verification Configuration". + +image::images/ssh-host-key-verification.png[Configure Ssh host key verification] + + [#bug-reports] == Bug Reports
src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java+9 −6 modified@@ -2006,6 +2006,7 @@ private String launchCommandWithCredentials(ArgumentListBuilder args, File workD File usernameFile = null; File passwordFile = null; File passphrase = null; + File knownHostsTemp = null; EnvVars env = environment; if (!PROMPT_FOR_AUTHENTICATION && isAtLeastVersion(2, 3, 0, 0)) { env = new EnvVars(env); @@ -2027,11 +2028,12 @@ private String launchCommandWithCredentials(ArgumentListBuilder args, File workD userName = sshUser.getUsername(); } passphrase = createPassphraseFile(sshUser); + knownHostsTemp = createTempFile("known_hosts",""); if (launcher.isUnix()) { - ssh = createUnixGitSSH(key, userName); + ssh = createUnixGitSSH(key, userName, knownHostsTemp); askpass = createUnixSshAskpass(sshUser, passphrase); } else { - ssh = createWindowsGitSSH(key, userName); + ssh = createWindowsGitSSH(key, userName, knownHostsTemp); askpass = createWindowsSshAskpass(sshUser, passphrase); } @@ -2103,6 +2105,7 @@ private String launchCommandWithCredentials(ArgumentListBuilder args, File workD deleteTempFile(passphrase); deleteTempFile(usernameFile); deleteTempFile(passwordFile); + deleteTempFile(knownHostsTemp); } } @@ -2538,20 +2541,20 @@ public File getSSHExecutable() { throw new RuntimeException("ssh executable not found. The git plugin only supports official git client https://git-scm.com/download/win"); } - private File createWindowsGitSSH(File key, String user) throws IOException { + private File createWindowsGitSSH(File key, String user, File knownHosts) throws IOException { File ssh = createTempFile("ssh", ".bat"); File sshexe = getSSHExecutable(); try (PrintWriter w = new PrintWriter(ssh, encoding)) { w.println("@echo off"); - w.println("\"" + sshexe.getAbsolutePath() + "\" -i \"" + key.getAbsolutePath() +"\" -l \"" + user + "\" -o StrictHostKeyChecking=no %* "); + w.println("\"" + sshexe.getAbsolutePath() + "\" -i \"" + key.getAbsolutePath() +"\" -l \"" + user + "\" " + getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + " %* "); } ssh.setExecutable(true, true); return ssh; } - private File createUnixGitSSH(File key, String user) throws IOException { + private File createUnixGitSSH(File key, String user, File knownHosts) throws IOException { File ssh = createTempFile("ssh", ".sh"); File ssh_copy = new File(ssh.toString() + "-copy"); boolean isCopied = false; @@ -2562,7 +2565,7 @@ private File createUnixGitSSH(File key, String user) throws IOException { w.println(" DISPLAY=:123.456"); w.println(" export DISPLAY"); w.println("fi"); - w.println("ssh -i \"" + key.getAbsolutePath() + "\" -l \"" + user + "\" -o StrictHostKeyChecking=no \"$@\""); + w.println("ssh -i \"" + key.getAbsolutePath() + "\" -l \"" + user + "\" " + getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + " \"$@\""); } ssh.setExecutable(true, true); //JENKINS-48258 git client plugin occasionally fails with "text file busy" error
src/main/java/org/jenkinsci/plugins/gitclient/GitHostKeyVerificationConfiguration.java+35 −0 added@@ -0,0 +1,35 @@ +package org.jenkinsci.plugins.gitclient; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.PersistentDescriptor; +import jenkins.model.GlobalConfiguration; +import jenkins.model.GlobalConfigurationCategory; +import org.jenkinsci.plugins.gitclient.verifier.HostKeyVerifierFactory; +import org.jenkinsci.plugins.gitclient.verifier.AcceptFirstConnectionStrategy; +import org.jenkinsci.plugins.gitclient.verifier.SshHostKeyVerificationStrategy; + +@Extension +public class GitHostKeyVerificationConfiguration extends GlobalConfiguration implements PersistentDescriptor { + + private SshHostKeyVerificationStrategy<? extends HostKeyVerifierFactory> sshHostKeyVerificationStrategy; + + @Override + public @NonNull + GlobalConfigurationCategory getCategory() { + return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class); + } + + public SshHostKeyVerificationStrategy<? extends HostKeyVerifierFactory> getSshHostKeyVerificationStrategy() { + return sshHostKeyVerificationStrategy != null ? sshHostKeyVerificationStrategy : new AcceptFirstConnectionStrategy(); + } + + public void setSshHostKeyVerificationStrategy(SshHostKeyVerificationStrategy<? extends HostKeyVerifierFactory> sshHostKeyVerificationStrategy) { + this.sshHostKeyVerificationStrategy = sshHostKeyVerificationStrategy; + save(); + } + + public static @NonNull GitHostKeyVerificationConfiguration get() { + return GlobalConfiguration.all().getInstance(GitHostKeyVerificationConfiguration.class); + } +}
src/main/java/org/jenkinsci/plugins/gitclient/Git.java+27 −4 modified@@ -10,12 +10,16 @@ import org.jenkinsci.plugins.gitclient.jgit.PreemptiveAuthHttpClientConnectionFactory; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.jenkinsci.plugins.gitclient.verifier.HostKeyVerifierFactory; +import org.jenkinsci.plugins.gitclient.verifier.NoHostKeyVerificationStrategy; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Git repository access class. Provides local and remote access to a git @@ -34,6 +38,9 @@ * @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a> */ public class Git implements Serializable { + + private static final Logger LOGGER = Logger.getLogger(Git.class.getName()); + private FilePath repository; private TaskListener listener; private EnvVars env; @@ -122,7 +129,14 @@ public Git using(String exe) { * @throws java.lang.InterruptedException if interrupted. */ public GitClient getClient() throws IOException, InterruptedException { - jenkins.MasterToSlaveFileCallable<GitClient> callable = new GitAPIMasterToSlaveFileCallable(); + HostKeyVerifierFactory hostKeyFactory; + if (Jenkins.getInstanceOrNull() == null) { + LOGGER.log(Level.FINE, "No Jenkins instance, skipping host key checking by default"); + hostKeyFactory = new NoHostKeyVerificationStrategy().getVerifier(); + } else { + hostKeyFactory = GitHostKeyVerificationConfiguration.get().getSshHostKeyVerificationStrategy().getVerifier(); + } + jenkins.MasterToSlaveFileCallable<GitClient> callable = new GitAPIMasterToSlaveFileCallable(hostKeyFactory); GitClient git = (repository!=null ? repository.act(callable) : callable.invoke(null,null)); Jenkins jenkinsInstance = Jenkins.getInstanceOrNull(); if (jenkinsInstance != null && git != null) @@ -152,6 +166,13 @@ private GitClient initMockClient(String className, String exe, EnvVars env, File private static final long serialVersionUID = 1L; private class GitAPIMasterToSlaveFileCallable extends jenkins.MasterToSlaveFileCallable<GitClient> { + + private final HostKeyVerifierFactory hostKeyFactory; + + public GitAPIMasterToSlaveFileCallable(HostKeyVerifierFactory hostKeyFactory) { + this.hostKeyFactory = hostKeyFactory; + } + public GitClient invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { if (listener == null) listener = TaskListener.NULL; if (env == null) env = new EnvVars(); @@ -162,15 +183,17 @@ public GitClient invoke(File f, VirtualChannel channel) throws IOException, Inte } if (exe == null || JGitTool.MAGIC_EXENAME.equalsIgnoreCase(exe)) { - return new JGitAPIImpl(f, listener); + return new JGitAPIImpl(f, listener, null, hostKeyFactory); } if (JGitApacheTool.MAGIC_EXENAME.equalsIgnoreCase(exe)) { final PreemptiveAuthHttpClientConnectionFactory factory = new PreemptiveAuthHttpClientConnectionFactory(); - return new JGitAPIImpl(f, listener, factory); + return new JGitAPIImpl(f, listener, factory, hostKeyFactory); } // Ensure we return a backward compatible GitAPI, even API only claim to provide a GitClient - return new GitAPI(exe, f, listener, env); + GitAPI gitAPI = new GitAPI(exe, f, listener, env); + gitAPI.setHostKeyFactory(hostKeyFactory); + return gitAPI; } } }
src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java+8 −2 modified@@ -123,6 +123,7 @@ import hudson.plugins.git.GitObject; import org.eclipse.jgit.api.RebaseCommand.Operation; import org.eclipse.jgit.api.RebaseResult; +import org.jenkinsci.plugins.gitclient.verifier.HostKeyVerifierFactory; /** * GitClient pure Java implementation using JGit. @@ -148,15 +149,20 @@ public class JGitAPIImpl extends LegacyCompatibleGitAPIImpl { this(workspace, listener, null); } + @Deprecated JGitAPIImpl(File workspace, TaskListener listener, final PreemptiveAuthHttpClientConnectionFactory httpConnectionFactory) { + this(workspace, listener, httpConnectionFactory, null); + } + + JGitAPIImpl(File workspace, TaskListener listener, final PreemptiveAuthHttpClientConnectionFactory httpConnectionFactory, HostKeyVerifierFactory hostKeyFactory) { /* If workspace is null, then default to current directory to match * CliGitAPIImpl behavior */ - super(workspace == null ? new File(".") : workspace); + super(workspace == null ? new File(".") : workspace, hostKeyFactory); this.listener = listener; // to avoid rogue plugins from clobbering what we use, always // make a point of overwriting it with ours. - SshSessionFactory.setInstance(new TrileadSessionFactory()); + SshSessionFactory.setInstance(new TrileadSessionFactory(hostKeyFactory, listener)); if (httpConnectionFactory != null) { httpConnectionFactory.setCredentialsProvider(asSmartCredentialsProvider());
src/main/java/org/jenkinsci/plugins/gitclient/LegacyCompatibleGitAPIImpl.java+17 −0 modified@@ -25,6 +25,7 @@ import java.util.Map; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.jenkinsci.plugins.gitclient.verifier.HostKeyVerifierFactory; /** * Partial implementation of {@link IGitAPI} by delegating to {@link GitClient} APIs. @@ -37,6 +38,8 @@ */ abstract class LegacyCompatibleGitAPIImpl extends AbstractGitAPIImpl implements IGitAPI { + private HostKeyVerifierFactory hostKeyFactory; + /** * isBareRepository. * @@ -56,10 +59,24 @@ public boolean isBareRepository() throws GitException, InterruptedException { * * @param workspace a {@link java.io.File} object. */ + @Deprecated protected LegacyCompatibleGitAPIImpl(File workspace) { this.workspace = workspace; } + protected LegacyCompatibleGitAPIImpl(File workspace, HostKeyVerifierFactory hostKeyFactory) { + this.workspace = workspace; + this.hostKeyFactory = hostKeyFactory; + } + + public HostKeyVerifierFactory getHostKeyFactory() { + return hostKeyFactory; + } + + public void setHostKeyFactory(HostKeyVerifierFactory verifier) { + this.hostKeyFactory = verifier; + } + /** {@inheritDoc} */ @Deprecated public boolean hasGitModules(String treeIsh) throws GitException {
src/main/java/org/jenkinsci/plugins/gitclient/trilead/JGitConnection.java+26 −0 added@@ -0,0 +1,26 @@ +package org.jenkinsci.plugins.gitclient.trilead; + +import com.trilead.ssh2.Connection; +import com.trilead.ssh2.ConnectionInfo; +import com.trilead.ssh2.ServerHostKeyVerifier; +import org.jenkinsci.plugins.gitclient.verifier.AbstractJGitHostKeyVerifier; + +import java.io.IOException; + +public class JGitConnection extends Connection { + + public JGitConnection(String hostname, int port) { + super(hostname, port); + } + + @Override + public ConnectionInfo connect(ServerHostKeyVerifier verifier) throws IOException { + if (verifier instanceof AbstractJGitHostKeyVerifier) { + String[] serverHostKeyAlgorithms = ((AbstractJGitHostKeyVerifier) verifier).getServerHostKeyAlgorithms(this); + if (serverHostKeyAlgorithms != null && serverHostKeyAlgorithms.length > 0) { + setServerHostKeyAlgorithms(serverHostKeyAlgorithms); + } + } + return super.connect(verifier); + } +}
src/main/java/org/jenkinsci/plugins/gitclient/trilead/TrileadSessionFactory.java+31 −2 modified@@ -2,31 +2,60 @@ import com.cloudbees.jenkins.plugins.sshcredentials.SSHAuthenticator; import com.trilead.ssh2.Connection; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import hudson.model.TaskListener; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.errors.UnsupportedCredentialItem; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.RemoteSession; import org.eclipse.jgit.transport.SshSessionFactory; import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.util.FS; +import org.jenkinsci.plugins.gitclient.verifier.AcceptFirstConnectionVerifier; +import org.jenkinsci.plugins.gitclient.verifier.HostKeyVerifierFactory; import java.io.IOException; +import java.util.concurrent.locks.ReentrantLock; /** * Makes JGit uses Trilead for connectivity. * * @author Kohsuke Kawaguchi */ public class TrileadSessionFactory extends SshSessionFactory { + + private static final ReentrantLock JGIT_ACCEPT_FIRST_LOCK = new ReentrantLock(); + + private final HostKeyVerifierFactory hostKeyVerifierFactory; + private final TaskListener listener; + + @SuppressFBWarnings(value = "EI_EXPOSE_REP2") + public TrileadSessionFactory(HostKeyVerifierFactory hostKeyVerifierFactory, TaskListener listener) { + this.hostKeyVerifierFactory = hostKeyVerifierFactory; + this.listener = listener; + } + /** {@inheritDoc} */ @Override public RemoteSession getSession(URIish uri, CredentialsProvider credentialsProvider, FS fs, int tms) throws TransportException { try { int p = uri.getPort(); if (p<0) p = 22; - Connection con = new Connection(uri.getHost(), p); + JGitConnection con = new JGitConnection(uri.getHost(), p); con.setTCPNoDelay(true); - con.connect(); // TODO: host key check + if (hostKeyVerifierFactory instanceof AcceptFirstConnectionVerifier) { + // Accept First connection behavior need to be synchronized, because it's the only verifier + // which could change (populate) known hosts dynamically, in other words AcceptFirstConnectionVerifier + // should be able to see and read if any known hosts was added during parallel connection. + JGIT_ACCEPT_FIRST_LOCK.lock(); + try { + con.connect(hostKeyVerifierFactory.forJGit(listener)); + } finally { + JGIT_ACCEPT_FIRST_LOCK.unlock(); + } + } else { + con.connect(hostKeyVerifierFactory.forJGit(listener)); + } boolean authenticated; if (credentialsProvider instanceof SmartCredentialsProvider) {
src/main/java/org/jenkinsci/plugins/gitclient/verifier/AbstractCliGitHostKeyVerifier.java+18 −0 added@@ -0,0 +1,18 @@ +package org.jenkinsci.plugins.gitclient.verifier; + +import org.jenkinsci.remoting.SerializableOnlyOverRemoting; + +import java.io.File; +import java.io.IOException; + +public interface AbstractCliGitHostKeyVerifier extends SerializableOnlyOverRemoting { + + /** + * Specifies Git command-line options that control the logic of this verifier. + * @param tempKnownHosts a temporary file that has already been created and may be used. + * @return the command-line options + * @throws IOException + */ + String getVerifyHostKeyOption(File tempKnownHosts) throws IOException; + +}
src/main/java/org/jenkinsci/plugins/gitclient/verifier/AbstractJGitHostKeyVerifier.java+57 −0 added@@ -0,0 +1,57 @@ +package org.jenkinsci.plugins.gitclient.verifier; + +import com.trilead.ssh2.Connection; +import com.trilead.ssh2.KnownHosts; +import com.trilead.ssh2.ServerHostKeyVerifier; +import hudson.model.TaskListener; +import org.jenkinsci.remoting.SerializableOnlyOverRemoting; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +public abstract class AbstractJGitHostKeyVerifier implements ServerHostKeyVerifier, SerializableOnlyOverRemoting { + + private static final Logger LOGGER = Logger.getLogger(AbstractJGitHostKeyVerifier.class.getName()); + + protected final transient KnownHosts knownHosts; + + protected AbstractJGitHostKeyVerifier(KnownHosts knownHosts) { + this.knownHosts = knownHosts; + } + + public abstract String[] getServerHostKeyAlgorithms(Connection connection) throws IOException; + + /** + * Defines host key algorithms which is used for a Connection while establishing an encrypted TCP/IP connection to a SSH-2 server. + * @param connection + * @return array of algorithms for a connection + * @throws IOException + */ + String[] getPreferredServerHostkeyAlgorithmOrder(Connection connection) { + String[] preferredServerHostkeyAlgorithmOrder = knownHosts.getPreferredServerHostkeyAlgorithmOrder(connection.getHostname()); + if (preferredServerHostkeyAlgorithmOrder == null) { + return knownHosts.getPreferredServerHostkeyAlgorithmOrder(connection.getHostname() + ":" + connection.getPort()); + } + return preferredServerHostkeyAlgorithmOrder; + } + + boolean verifyServerHostKey(TaskListener taskListener, KnownHosts knownHosts, String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException { + String hostPort = hostname + ":" + port; + int resultHost = knownHosts.verifyHostkey(hostname, serverHostKeyAlgorithm, serverHostKey); + int resultHostPort = knownHosts.verifyHostkey(hostPort, serverHostKeyAlgorithm, serverHostKey); + boolean isValid = KnownHosts.HOSTKEY_IS_OK == resultHost || KnownHosts.HOSTKEY_IS_OK == resultHostPort; + + if (!isValid) { + LOGGER.log(Level.WARNING, "Host key {0} was not accepted.", hostPort); + taskListener.getLogger().printf("Host key for host %s was not accepted.%n", hostPort); + } + + return isValid; + } + + KnownHosts getKnownHosts() { + return knownHosts; + } + +}
src/main/java/org/jenkinsci/plugins/gitclient/verifier/AcceptFirstConnectionStrategy.java+30 −0 added@@ -0,0 +1,30 @@ +package org.jenkinsci.plugins.gitclient.verifier; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.Descriptor; +import org.kohsuke.stapler.DataBoundConstructor; + +public class AcceptFirstConnectionStrategy extends SshHostKeyVerificationStrategy<AcceptFirstConnectionVerifier> { + + @DataBoundConstructor + public AcceptFirstConnectionStrategy() { + // stapler needs @DataBoundConstructor + } + + @Override + public AcceptFirstConnectionVerifier getVerifier() { + return new AcceptFirstConnectionVerifier(); + } + + @Extension + public static class AcceptFirstConnectionStrategyDescriptor extends Descriptor<SshHostKeyVerificationStrategy<AcceptFirstConnectionVerifier>> { + + @NonNull + @Override + public String getDisplayName() { + return "Accept first connection"; + } + + } +}
src/main/java/org/jenkinsci/plugins/gitclient/verifier/AcceptFirstConnectionVerifier.java+93 −0 added@@ -0,0 +1,93 @@ +package org.jenkinsci.plugins.gitclient.verifier; + +import com.trilead.ssh2.Connection; +import com.trilead.ssh2.KnownHosts; +import hudson.model.TaskListener; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class AcceptFirstConnectionVerifier extends HostKeyVerifierFactory { + + private static final Logger LOGGER = Logger.getLogger(AcceptFirstConnectionVerifier.class.getName()); + + @Override + public AbstractCliGitHostKeyVerifier forCliGit(TaskListener listener) { + return tempKnownHosts -> { + listener.getLogger().println("Verifying host key using known hosts file, will automatically accept unseen keys"); + return "-o StrictHostKeyChecking=accept-new -o HashKnownHosts=yes"; + }; + } + + @Override + public AbstractJGitHostKeyVerifier forJGit(TaskListener listener) { + KnownHosts knownHosts; + try { + knownHosts = Files.exists(getKnownHostsFile().toPath()) ? new KnownHosts(getKnownHostsFile()) : new KnownHosts(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, e, () -> "Could not load known hosts."); + knownHosts = new KnownHosts(); + } + return new AcceptFirstConnectionJGitHostKeyVerifier(listener, knownHosts); + } + + public class AcceptFirstConnectionJGitHostKeyVerifier extends AbstractJGitHostKeyVerifier { + + private final TaskListener listener; + + public AcceptFirstConnectionJGitHostKeyVerifier(TaskListener listener, KnownHosts knownHosts) { + super(knownHosts); + this.listener = listener; + } + + @Override + public String[] getServerHostKeyAlgorithms(Connection connection) throws IOException { + return getPreferredServerHostkeyAlgorithmOrder(connection); + } + + @Override + public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws Exception { + listener.getLogger().printf("Verifying host key for %s using %s %n", hostname, getKnownHostsFile().toPath()); + File knownHostsFile = getKnownHostsFile(); + Path path = Paths.get(knownHostsFile.getAbsolutePath()); + String hostnamePort = hostname + ":" + port; + boolean isValid = false; + if (Files.notExists(path)) { + Files.createDirectories(knownHostsFile.getParentFile().toPath()); + Files.createFile(path); + listener.getLogger().println("Creating new known hosts file " + path); + writeToFile(knownHostsFile, hostnamePort, serverHostKeyAlgorithm, serverHostKey); + isValid = true; + } else { + KnownHosts knownHosts = getKnownHosts(); + int hostPortResult = knownHosts.verifyHostkey(hostnamePort, serverHostKeyAlgorithm, serverHostKey); + if (KnownHosts.HOSTKEY_IS_OK == hostPortResult || KnownHosts.HOSTKEY_IS_OK == knownHosts.verifyHostkey(hostname, serverHostKeyAlgorithm, serverHostKey)) { + isValid = true; + } else if (KnownHosts.HOSTKEY_IS_NEW == hostPortResult) { + writeToFile(knownHostsFile, hostnamePort, serverHostKeyAlgorithm, serverHostKey); + isValid = true; + } + } + + if (!isValid) { + LOGGER.log(Level.WARNING, "Host key {0} was not accepted.", hostnamePort); + listener.getLogger().printf("Host key for host %s was not accepted.%n", hostnamePort); + } + + return isValid; + + } + + private void writeToFile(File knownHostsFile, String hostnamePort, String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException { + listener.getLogger().println("Adding " + hostnamePort + " to " + knownHostsFile.toPath()); + KnownHosts.addHostkeyToFile(knownHostsFile, new String[]{KnownHosts.createHashedHostname(hostnamePort)}, serverHostKeyAlgorithm, serverHostKey); + getKnownHosts().addHostkey(new String[]{KnownHosts.createHashedHostname(hostnamePort)}, serverHostKeyAlgorithm, serverHostKey); + } + } + +}
src/main/java/org/jenkinsci/plugins/gitclient/verifier/HostKeyVerifierFactory.java+24 −0 added@@ -0,0 +1,24 @@ +package org.jenkinsci.plugins.gitclient.verifier; + +import hudson.model.TaskListener; +import org.jenkinsci.remoting.SerializableOnlyOverRemoting; + +import java.io.File; + +public abstract class HostKeyVerifierFactory implements SerializableOnlyOverRemoting { + + /** + * @return Implementation of verifier for Command line git + */ + public abstract AbstractCliGitHostKeyVerifier forCliGit(TaskListener listener); + + /** + * @return Implementation of verifier for JGit + */ + public abstract AbstractJGitHostKeyVerifier forJGit(TaskListener listener); + + File getKnownHostsFile() { + return SshHostKeyVerificationStrategy.JGIT_KNOWN_HOSTS_FILE; + } + +}
src/main/java/org/jenkinsci/plugins/gitclient/verifier/KnownHostsFileVerificationStrategy.java+30 −0 added@@ -0,0 +1,30 @@ +package org.jenkinsci.plugins.gitclient.verifier; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.Descriptor; +import org.kohsuke.stapler.DataBoundConstructor; + +public class KnownHostsFileVerificationStrategy extends SshHostKeyVerificationStrategy<KnownHostsFileVerifier> { + + @DataBoundConstructor + public KnownHostsFileVerificationStrategy() { + // stapler needs @DataBoundConstructor + } + + @Override + public KnownHostsFileVerifier getVerifier() { + return new KnownHostsFileVerifier(); + } + + @Extension + public static class KnownHostsFileVerificationStrategyDescriptor extends Descriptor<SshHostKeyVerificationStrategy<KnownHostsFileVerifier>> { + + @NonNull + @Override + public String getDisplayName() { + return "Known hosts file"; + } + + } +}
src/main/java/org/jenkinsci/plugins/gitclient/verifier/KnownHostsFileVerifier.java+57 −0 added@@ -0,0 +1,57 @@ +package org.jenkinsci.plugins.gitclient.verifier; + +import com.trilead.ssh2.Connection; +import com.trilead.ssh2.KnownHosts; +import hudson.model.TaskListener; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class KnownHostsFileVerifier extends HostKeyVerifierFactory { + + private static final Logger LOGGER = Logger.getLogger(KnownHostsFileVerifier.class.getName()); + + @Override + public AbstractCliGitHostKeyVerifier forCliGit(TaskListener listener) { + return tempKnownHosts -> { + listener.getLogger().println("Verifying host key using known hosts file"); + return "-o StrictHostKeyChecking=yes"; + }; + } + + @Override + public AbstractJGitHostKeyVerifier forJGit(TaskListener listener) { + KnownHosts knownHosts; + try { + knownHosts = Files.exists(getKnownHostsFile().toPath()) ? new KnownHosts(getKnownHostsFile()) : new KnownHosts(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, e, () -> "Could not load known hosts."); + knownHosts = new KnownHosts(); + } + return new KnownHostsFileJGitHostKeyVerifier(listener, knownHosts); + } + + public class KnownHostsFileJGitHostKeyVerifier extends AbstractJGitHostKeyVerifier { + + private final TaskListener listener; + + public KnownHostsFileJGitHostKeyVerifier(TaskListener listener, KnownHosts knownHosts) { + super(knownHosts); + this.listener = listener; + } + + @Override + public String[] getServerHostKeyAlgorithms(Connection connection) throws IOException { + return getPreferredServerHostkeyAlgorithmOrder(connection); + } + + @Override + public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws Exception { + listener.getLogger().printf("Verifying host key for %s using %s %n", hostname, getKnownHostsFile().toPath()); + return verifyServerHostKey(listener, getKnownHosts(), hostname, port, serverHostKeyAlgorithm, serverHostKey); + } + } + +}
src/main/java/org/jenkinsci/plugins/gitclient/verifier/ManuallyProvidedKeyVerificationStrategy.java+57 −0 added@@ -0,0 +1,57 @@ +package org.jenkinsci.plugins.gitclient.verifier; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.Descriptor; +import hudson.util.FormValidation; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; + +import java.util.Objects; + +public class ManuallyProvidedKeyVerificationStrategy extends SshHostKeyVerificationStrategy<ManuallyProvidedKeyVerifier> { + + private final String approvedHostKeys; + + @DataBoundConstructor + public ManuallyProvidedKeyVerificationStrategy(String approvedHostKeys) { + this.approvedHostKeys = approvedHostKeys.trim(); + } + + @Override + public ManuallyProvidedKeyVerifier getVerifier() { + return new ManuallyProvidedKeyVerifier(approvedHostKeys); + } + + public String getApprovedHostKeys() { + return approvedHostKeys; + } + + @Extension + public static class ManuallyTrustedKeyVerificationStrategyDescriptor extends Descriptor<SshHostKeyVerificationStrategy<ManuallyProvidedKeyVerifier>> { + + @NonNull + @Override + public String getDisplayName() { + return "Manually provided keys"; + } + + public FormValidation doCheckApprovedHostKeys(@QueryParameter String approvedHostKeys) { + return FormValidation.validateRequired(approvedHostKeys); + } + + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ManuallyProvidedKeyVerificationStrategy that = (ManuallyProvidedKeyVerificationStrategy) o; + return Objects.equals(approvedHostKeys, that.approvedHostKeys); + } + + @Override + public int hashCode() { + return Objects.hash(approvedHostKeys); + } +}
src/main/java/org/jenkinsci/plugins/gitclient/verifier/ManuallyProvidedKeyVerifier.java+74 −0 added@@ -0,0 +1,74 @@ +package org.jenkinsci.plugins.gitclient.verifier; + +import com.trilead.ssh2.Connection; +import com.trilead.ssh2.KnownHosts; +import hudson.model.TaskListener; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class ManuallyProvidedKeyVerifier extends HostKeyVerifierFactory { + + private static final Logger LOGGER = Logger.getLogger(ManuallyProvidedKeyVerifier.class.getName()); + + private final String approvedHostKeys; + + public ManuallyProvidedKeyVerifier(String approvedHostKeys) { + this.approvedHostKeys = approvedHostKeys; + } + + @Override + public AbstractCliGitHostKeyVerifier forCliGit(TaskListener listener) { + return tempKnownHosts -> { + Files.write(tempKnownHosts.toPath(), (approvedHostKeys + System.lineSeparator()).getBytes(StandardCharsets.UTF_8)); + listener.getLogger().println("Verifying host key using manually-configured host key entries"); + String userKnownHostsFileFlag; + if (File.pathSeparatorChar == ';') { // check whether on Windows or not without sending Functions over remoting + // no escaping for windows because created temp file can't contain spaces + userKnownHostsFileFlag = String.format(" -o UserKnownHostsFile=%s", tempKnownHosts.getAbsolutePath()); + } else { + // escaping needed in case job name contains spaces + userKnownHostsFileFlag = String.format(" -o UserKnownHostsFile=\\\"\"\"%s\\\"\"\"", tempKnownHosts.getAbsolutePath().replace(" ", "\\ ")); + } + return "-o StrictHostKeyChecking=yes " + userKnownHostsFileFlag; + }; + } + + @Override + public AbstractJGitHostKeyVerifier forJGit(TaskListener listener) { + KnownHosts knownHosts; + try { + knownHosts = approvedHostKeys != null ? new KnownHosts(approvedHostKeys.toCharArray()) : new KnownHosts(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, e, () -> "Could not load known hosts."); + knownHosts = new KnownHosts(); + } + return new ManuallyProvidedKeyJGitHostKeyVerifier(listener, knownHosts); + } + + public static class ManuallyProvidedKeyJGitHostKeyVerifier extends AbstractJGitHostKeyVerifier { + + private final TaskListener listener; + + public ManuallyProvidedKeyJGitHostKeyVerifier(TaskListener listener, KnownHosts knownHosts) { + super(knownHosts); + this.listener = listener; + } + + @Override + public String[] getServerHostKeyAlgorithms(Connection connection) throws IOException { + return getPreferredServerHostkeyAlgorithmOrder(connection); + } + + @Override + public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws Exception { + listener.getLogger().printf("Verifying host key for %s using manually-configured host key entries %n", hostname); + return verifyServerHostKey(listener, getKnownHosts(), hostname, port, serverHostKeyAlgorithm, serverHostKey); + } + } + +}
src/main/java/org/jenkinsci/plugins/gitclient/verifier/NoHostKeyVerificationStrategy.java+31 −0 added@@ -0,0 +1,31 @@ +package org.jenkinsci.plugins.gitclient.verifier; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.Descriptor; +import org.kohsuke.stapler.DataBoundConstructor; + +@Extension +public class NoHostKeyVerificationStrategy extends SshHostKeyVerificationStrategy<NoHostKeyVerifier> { + + @DataBoundConstructor + public NoHostKeyVerificationStrategy() { + super(); + } + + @Override + public NoHostKeyVerifier getVerifier() { + return new NoHostKeyVerifier(); + } + + @Extension + public static class NoHostKeyVerificationStrategyDescriptor extends Descriptor<SshHostKeyVerificationStrategy<NoHostKeyVerifier>> { + + @NonNull + @Override + public String getDisplayName() { + return "No verification"; + } + + } +}
src/main/java/org/jenkinsci/plugins/gitclient/verifier/NoHostKeyVerifier.java+30 −0 added@@ -0,0 +1,30 @@ +package org.jenkinsci.plugins.gitclient.verifier; + +import com.trilead.ssh2.Connection; +import com.trilead.ssh2.KnownHosts; +import hudson.model.TaskListener; + +public class NoHostKeyVerifier extends HostKeyVerifierFactory { + + @Override + public AbstractCliGitHostKeyVerifier forCliGit(TaskListener listener) { + return tempKnownHosts -> "-o StrictHostKeyChecking=no"; + } + + @Override + public AbstractJGitHostKeyVerifier forJGit(TaskListener listener) { + return new AbstractJGitHostKeyVerifier(new KnownHosts()) { + + @Override + public String[] getServerHostKeyAlgorithms(Connection connection) { + return new String[0]; + } + + @Override + public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) { + return true; + } + }; + } + +}
src/main/java/org/jenkinsci/plugins/gitclient/verifier/SshHostKeyVerificationStrategy.java+26 −0 added@@ -0,0 +1,26 @@ +package org.jenkinsci.plugins.gitclient.verifier; + +import hudson.ExtensionPoint; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Descriptor; +import jenkins.model.Jenkins; +import org.apache.commons.lang.StringUtils; + +import java.io.File; +import java.nio.file.Paths; + +public abstract class SshHostKeyVerificationStrategy<T extends HostKeyVerifierFactory> extends AbstractDescribableImpl<SshHostKeyVerificationStrategy<T>> implements ExtensionPoint { + + private static final String KNOWN_HOSTS_DEFAULT = Paths.get(System.getProperty("user.home"), ".ssh", "known_hosts").toString(); + private static final String JGIT_KNOWN_HOSTS_PROPERTY = SshHostKeyVerificationStrategy.class.getName() + ".jgit_known_hosts_file"; + private static final String JGIT_KNOWN_HOSTS_FILE_PATH = StringUtils.defaultIfBlank(System.getProperty(JGIT_KNOWN_HOSTS_PROPERTY), KNOWN_HOSTS_DEFAULT); + public static final File JGIT_KNOWN_HOSTS_FILE = new File(JGIT_KNOWN_HOSTS_FILE_PATH); + + @Override + public Descriptor<SshHostKeyVerificationStrategy<T>> getDescriptor() { + return (Descriptor<SshHostKeyVerificationStrategy<T>>) Jenkins.get().getDescriptorOrDie(getClass()); + } + + public abstract T getVerifier(); + +}
src/main/resources/org/jenkinsci/plugins/gitclient/GitHostKeyVerificationConfiguration/config.jelly+6 −0 added@@ -0,0 +1,6 @@ +<?jelly escape-by-default='true'?> +<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form"> + <f:section title="Git Host Key Verification Configuration"> + <f:dropdownDescriptorSelector field="sshHostKeyVerificationStrategy" title="Host Key Verification Strategy"/> + </f:section> +</j:jelly> \ No newline at end of file
src/main/resources/org/jenkinsci/plugins/gitclient/GitHostKeyVerificationConfiguration/help-sshHostKeyVerificationStrategy.html+13 −0 added@@ -0,0 +1,13 @@ +<p>Controls how Git plugin verifies the keys presented by the host during SSH connecting. +<dl> + <dt>Accept first connection (default)</dt> + <dd>Automatically adds host keys to the <code>known_hosts</code> file if the host has not been seen before, and does not allow connections to previously-seen hosts with modified keys.</dd> + <dd> - Note that when using ephemeral agents (ex. cloud agents), this strategy is essentially equivalent to <strong>No verification</strong> because it uses the <code>known_hosts</code> file on the agent. To avoid this, you can pre-configure <code>known_hosts</code> with all relevant hosts when creating the images or templates used to define your agents, or use the <strong>Manually provided keys</strong> or <strong>Known hosts file</strong> strategies.</dd> + <dd> - OpenSSH version 7.6 or higher is required to use this option with command line Git.</dd> + <dt>Known hosts file</dt> + <dd>Verifies all host keys using the <code>known_hosts</code> file.</dd> + <dt>Manually provided keys</dt> + <dd>Verifies all host keys using a set of keys manually configured here.</dd> + <dt>No verification (not recommended)</dt> + <dd>Does not verify host keys at all.</dd> +</dl>
src/main/resources/org/jenkinsci/plugins/gitclient/verifier/ManuallyProvidedKeyVerificationStrategy/config.jelly+6 −0 added@@ -0,0 +1,6 @@ +<?jelly escape-by-default='true'?> +<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form"> + <f:entry title="${%Approved Host Keys}" field="approvedHostKeys"> + <f:textarea /> + </f:entry> +</j:jelly>
src/main/resources/org/jenkinsci/plugins/gitclient/verifier/ManuallyProvidedKeyVerificationStrategy/help-approvedHostKeys.html+11 −0 added@@ -0,0 +1,11 @@ +<div> + The host keys which will be allowed. + All other hosts and any modified host keys will be rejected. + The format is the same as for <code>known_hosts</code>, with one host per line. + For example: + <pre> + github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl + bitbucket.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl + </pre> + +</div> \ No newline at end of file
src/main/resources/org/jenkinsci/plugins/gitclient/verifier/NoHostKeyVerificationStrategy/config.jelly+4 −0 added@@ -0,0 +1,4 @@ +<?jelly escape-by-default='true'?> +<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form"> + <f:description>${%blurb}</f:description> +</j:jelly>
src/main/resources/org/jenkinsci/plugins/gitclient/verifier/NoHostKeyVerificationStrategy/config.properties+1 −0 added@@ -0,0 +1 @@ +blurb=<div class="warning">This option is generally insecure. Host key will not be verified during SSH connection</div> \ No newline at end of file
src/test/java/org/jenkinsci/plugins/gitclient/GitHostKeyVerificationConfigurationTest.java+52 −0 added@@ -0,0 +1,52 @@ +package org.jenkinsci.plugins.gitclient; + +import org.jenkinsci.plugins.gitclient.verifier.AcceptFirstConnectionStrategy; +import org.jenkinsci.plugins.gitclient.verifier.KnownHostsFileVerificationStrategy; +import org.jenkinsci.plugins.gitclient.verifier.ManuallyProvidedKeyVerificationStrategy; +import org.jenkinsci.plugins.gitclient.verifier.NoHostKeyVerificationStrategy; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.RestartableJenkinsRule; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class GitHostKeyVerificationConfigurationTest { + + @Rule + public RestartableJenkinsRule r = new RestartableJenkinsRule(); + + private final GitHostKeyVerificationConfiguration gitHostKeyVerificationConfiguration; + + public GitHostKeyVerificationConfigurationTest() { + gitHostKeyVerificationConfiguration = new GitHostKeyVerificationConfiguration(); + } + + @Test + public void testGetSshHostKeyVerificationStrategyInitiallyAcceptFirst() { + assertThat(gitHostKeyVerificationConfiguration.getSshHostKeyVerificationStrategy(), instanceOf(AcceptFirstConnectionStrategy.class)); + } + + @Test + public void testGitHostKeyVerificationConfigurationSavedBetweenSessions() throws Exception { + String hostKey = "github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"; + ManuallyProvidedKeyVerificationStrategy manuallyProvidedKeyVerificationStrategy = new ManuallyProvidedKeyVerificationStrategy(hostKey); + r.then(step -> { + assertThat(GitHostKeyVerificationConfiguration.get().getSshHostKeyVerificationStrategy(), instanceOf(AcceptFirstConnectionStrategy.class)); + GitHostKeyVerificationConfiguration.get().setSshHostKeyVerificationStrategy(manuallyProvidedKeyVerificationStrategy); + }); + + r.then(step -> { + assertThat(GitHostKeyVerificationConfiguration.get().getSshHostKeyVerificationStrategy(), is(manuallyProvidedKeyVerificationStrategy)); + GitHostKeyVerificationConfiguration.get().setSshHostKeyVerificationStrategy(new NoHostKeyVerificationStrategy()); + }); + + r.then(step -> { + assertThat(GitHostKeyVerificationConfiguration.get().getSshHostKeyVerificationStrategy(), instanceOf(NoHostKeyVerificationStrategy.class)); + GitHostKeyVerificationConfiguration.get().setSshHostKeyVerificationStrategy(new KnownHostsFileVerificationStrategy()); + }); + + r.then(step -> assertThat(GitHostKeyVerificationConfiguration.get().getSshHostKeyVerificationStrategy(), instanceOf(KnownHostsFileVerificationStrategy.class))); + } +}
src/test/java/org/jenkinsci/plugins/gitclient/verifier/AcceptFirstConnectionVerifierTest.java+172 −0 added@@ -0,0 +1,172 @@ +package org.jenkinsci.plugins.gitclient.verifier; + +import hudson.model.TaskListener; +import org.jenkinsci.plugins.gitclient.trilead.JGitConnection; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +public class AcceptFirstConnectionVerifierTest { + + private static final String FILE_CONTENT = "|1|4MiAohNAs5mYhPnYkpnOUWXmMTA=|iKR8xF3kCEdmSch/QtdXfdjWMCo=" + + " ssh-ed25519" + + " AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"; + + @Rule + public TemporaryFolder testFolder = TemporaryFolder.builder().assureDeletion().build(); + private final KnownHostsTestUtil knownHostsTestUtil = new KnownHostsTestUtil(testFolder); + + @Test + public void testVerifyHostKeyOption() throws IOException { + assertThat(new AcceptFirstConnectionVerifier().forCliGit(TaskListener.NULL).getVerifyHostKeyOption(null), is("-o StrictHostKeyChecking=accept-new -o HashKnownHosts=yes")); + } + + @Test + public void testVerifyServerHostKeyWhenFirstConnection() { + File file = new File(testFolder.getRoot() + "path/to/file"); + AcceptFirstConnectionVerifier acceptFirstConnectionVerifier = spy(new AcceptFirstConnectionVerifier()); + when(acceptFirstConnectionVerifier.getKnownHostsFile()).thenReturn(file); + AbstractJGitHostKeyVerifier verifier = acceptFirstConnectionVerifier.forJGit(TaskListener.NULL); + JGitConnection jGitConnection = new JGitConnection("github.com", 22); + + try { + jGitConnection.connect(verifier); + assertTrue(file.exists()); + assertThat(Files.readAllLines(file.toPath()), hasItem(containsString(FILE_CONTENT.substring(FILE_CONTENT.indexOf(" "))))); + } catch (IOException e) { + fail("Should not fail because first connection and create a file"); + } + } + + @Test + public void testVerifyServerHostKeyWhenSecondConnectionWithEqualKeys() throws Exception { + // |1|I9eFW1PcZ6UvKPt6iHmYwTXTo54=|PyasyFX5Az4w9co6JTn7rHkeFII= is github.com:22 + String hostKeyEntry = "|1|I9eFW1PcZ6UvKPt6iHmYwTXTo54=|PyasyFX5Az4w9co6JTn7rHkeFII= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"; + File mockedKnownHosts = knownHostsTestUtil.createFakeKnownHosts(hostKeyEntry); + AcceptFirstConnectionVerifier acceptFirstConnectionVerifier = spy(new AcceptFirstConnectionVerifier()); + when(acceptFirstConnectionVerifier.getKnownHostsFile()).thenReturn(mockedKnownHosts); + AbstractJGitHostKeyVerifier verifier = acceptFirstConnectionVerifier.forJGit(TaskListener.NULL); + JGitConnection jGitConnection = new JGitConnection("github.com", 22); + + try { + jGitConnection.connect(verifier); + assertThat(Files.readAllLines(mockedKnownHosts.toPath()), is(Collections.singletonList(hostKeyEntry))); + } catch (IOException e) { + fail("Should connect and do not add new line because keys are equal"); + } + } + + @Test + public void testVerifyServerHostKeyWhenHostnameWithoutPort() throws Exception { + String hostKeyEntry = "github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"; + File mockedKnownHosts = knownHostsTestUtil.createFakeKnownHosts(hostKeyEntry); + AcceptFirstConnectionVerifier acceptFirstConnectionVerifier = spy(new AcceptFirstConnectionVerifier()); + when(acceptFirstConnectionVerifier.getKnownHostsFile()).thenReturn(mockedKnownHosts); + AbstractJGitHostKeyVerifier verifier = acceptFirstConnectionVerifier.forJGit(TaskListener.NULL); + JGitConnection jGitConnection = new JGitConnection("github.com", 22); + + try { + jGitConnection.connect(verifier); + assertThat(Files.readAllLines(mockedKnownHosts.toPath()), is(Collections.singletonList(hostKeyEntry))); + } catch (IOException e) { + fail("Should connect and do not add new line because keys are equal"); + } + } + + @Test + public void testVerifyServerHostKeyWhenSecondConnectionWhenNotDefaultAlgorithm() throws Exception { + String fileContent = "github.com,140.82.121.4" + + " ecdsa-sha2-nistp256" + + " AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg="; + File mockedKnownHosts = knownHostsTestUtil.createFakeKnownHosts(fileContent); + AcceptFirstConnectionVerifier acceptFirstConnectionVerifier = spy(new AcceptFirstConnectionVerifier()); + when(acceptFirstConnectionVerifier.getKnownHostsFile()).thenReturn(mockedKnownHosts); + AbstractJGitHostKeyVerifier verifier = acceptFirstConnectionVerifier.forJGit(TaskListener.NULL); + JGitConnection jGitConnection = new JGitConnection("github.com", 22); + + try { + jGitConnection.connect(verifier); + assertThat(Files.readAllLines(mockedKnownHosts.toPath()), is(Collections.singletonList(fileContent))); + } catch (IOException e) { + fail("Should connect and do not add new line because keys are equal"); + } + } + + @Test + public void testVerifyServerHostKeyWhenSecondConnectionWithNonEqualKeys() throws Exception { + String fileContent = "|1|f7esvmtaiBk+EMHjPzWbRYRpBPY=|T7Qe4QAksYPZPwYEx5QxQykSjfc=" //github.com:22 + + " ssh-ed25519" + + " AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9OOOO"; + File mockedKnownHosts = knownHostsTestUtil.createFakeKnownHosts(fileContent); // file was created during first connection + AcceptFirstConnectionVerifier acceptFirstConnectionVerifier = spy(new AcceptFirstConnectionVerifier()); + when(acceptFirstConnectionVerifier.getKnownHostsFile()).thenReturn(mockedKnownHosts); + AbstractJGitHostKeyVerifier verifier = acceptFirstConnectionVerifier.forJGit(TaskListener.NULL); + JGitConnection jGitConnection = new JGitConnection("github.com", 22); + + try { + jGitConnection.connect(verifier); + fail("Should fail because hostkey is broken for 'github.com:22'"); + } catch (IOException e) { + assertThat(e.getMessage(), is("There was a problem while connecting to github.com:22")); + } + } + + @Test + public void testVerifyServerHostKeyWhenConnectionWithAnotherHost() throws Exception { + String bitbucketFileContent = "|1|HnmPCP38pBhCY0NUtBXSraOg9pM=|L6YZ9asEeb2xplTDEThGOxRq7ZY=" + + " ssh-rsa" + + " AAAAB3NzaC1yc2EAAAABIwAAAQEAubiN81eDcafrgMeLzaFPsw2kNvEcqTKl/VqLat/MaB33pZy0y3rJZtnqwR2qOOvbwKZYKiEO1O6VqNEBxKvJJelCq0dTXWT5pbO2gDXC6h6QDXCaHo6pOHGPUy+YBaGQRGuSusMEASYiWunYN0vCAI8QaXnWMXNMdFP3jHAJH0eDsoiGnLPBlBp4TNm6rYI74nMzgz3B9IikW4WVK+dc8KZJZWYjAuORU3jc1c/NPskD2ASinf8v3xnfXeukU0sJ5N6m5E8VLjObPEO+mN2t/FZTMZLiFqPWc/ALSqnMnnhwrNi2rbfg/rd/IpL8Le3pSBne8+seeFVBoGqzHM9yXw=="; + + File fakeKnownHosts = knownHostsTestUtil.createFakeKnownHosts(bitbucketFileContent); + AcceptFirstConnectionVerifier acceptFirstConnectionVerifier = spy(new AcceptFirstConnectionVerifier()); + when(acceptFirstConnectionVerifier.getKnownHostsFile()).thenReturn(fakeKnownHosts); + AbstractJGitHostKeyVerifier verifier = acceptFirstConnectionVerifier.forJGit(TaskListener.NULL); + JGitConnection jGitConnection = new JGitConnection("github.com", 22); + + try { + jGitConnection.connect(verifier); + List<String> actual = Files.readAllLines(fakeKnownHosts.toPath()); + assertThat(actual, hasItem(bitbucketFileContent)); + assertThat(actual, hasItem(containsString(FILE_CONTENT.substring(FILE_CONTENT.indexOf(" "))))); + } catch (IOException e) { + fail("Should connect and add new line because a new key"); + } + } + + @Test + public void testVerifyServerHostKeyWhenHostnamePortProvided() throws Exception { + String fileContent = "|1|6uMj3M7sLgZpn54vQbGqgPNTCVM=|OkV9Lu9REJZR5QCVrITAIY34I1M=" //github.com:59666 + + " ssh-ed25519" + + " AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"; + File mockedKnownHosts = knownHostsTestUtil.createFakeKnownHosts(fileContent); + AcceptFirstConnectionVerifier acceptFirstConnectionVerifier = spy(new AcceptFirstConnectionVerifier()); + when(acceptFirstConnectionVerifier.getKnownHostsFile()).thenReturn(mockedKnownHosts); + AbstractJGitHostKeyVerifier verifier = acceptFirstConnectionVerifier.forJGit(TaskListener.NULL); + JGitConnection jGitConnection = new JGitConnection("github.com", 22); + + try { + jGitConnection.connect(verifier); + List<String> actual = Files.readAllLines(mockedKnownHosts.toPath()); + assertThat(actual, hasItem(fileContent)); + assertThat(actual, hasItem(containsString(FILE_CONTENT.substring(FILE_CONTENT.indexOf(" "))))); + } catch (IOException e) { + fail("Should connect and add new line because a new key"); + } + } + +}
src/test/java/org/jenkinsci/plugins/gitclient/verifier/KnownHostsFileVerifierTest.java+94 −0 added@@ -0,0 +1,94 @@ +package org.jenkinsci.plugins.gitclient.verifier; + +import java.io.File; +import java.io.IOException; + +import hudson.model.TaskListener; +import org.jenkinsci.plugins.gitclient.trilead.JGitConnection; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class KnownHostsFileVerifierTest { + + private static final String FILE_CONTENT = "|1|MMHhyJWbis6eLbmW7/vVMgWL01M=|OT564q9RmLIALJ94imtE4PaCewU=" + + " ssh-ed25519" + + " AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"; + + // Create a temporary folder and assert folder deletion at end of tests + @Rule + public TemporaryFolder testFolder = TemporaryFolder.builder().assureDeletion().build(); + + private File fakeKnownHosts; + + private final KnownHostsTestUtil knownHostsTestUtil = new KnownHostsTestUtil(testFolder); + + + @Before + public void assignVerifiers() throws IOException { + fakeKnownHosts = knownHostsTestUtil.createFakeKnownHosts(FILE_CONTENT); + } + + @Test + public void connectWhenHostKeyNotInKnownHostsFileForOtherHostNameThenShouldFail() throws IOException { + fakeKnownHosts = knownHostsTestUtil.createFakeKnownHosts("fake2.ssh", "known_hosts_fake2", FILE_CONTENT); + KnownHostsFileVerifier knownHostsFileVerifier = spy(new KnownHostsFileVerifier()); + when(knownHostsFileVerifier.getKnownHostsFile()).thenReturn(fakeKnownHosts); + AbstractJGitHostKeyVerifier verifier = knownHostsFileVerifier.forJGit(TaskListener.NULL); + JGitConnection jGitConnection = new JGitConnection("bitbucket.org", 22); + + try { + jGitConnection.connect(verifier); + fail("Should fail because hostkey for 'bitbucket.org:22' is not in known_hosts file"); + } catch (IOException e) { + assertThat(e.getMessage(), is("There was a problem while connecting to bitbucket.org:22")); + } + } + + @Test + public void connectWhenHostKeyProvidedThenShouldNotFail() { + KnownHostsFileVerifier knownHostsFileVerifier = spy(new KnownHostsFileVerifier()); + when(knownHostsFileVerifier.getKnownHostsFile()).thenReturn(fakeKnownHosts); + AbstractJGitHostKeyVerifier verifier = knownHostsFileVerifier.forJGit(TaskListener.NULL); + JGitConnection jGitConnection = new JGitConnection("github.com", 22); + + try { + jGitConnection.connect(verifier); + } catch (IOException e) { + fail("Should not fail because hostkey for 'github.com:22' is in known_hosts"); + } + } + + @Test + public void connectWhenHostKeyInKnownHostsFileWithNotDefaultAlgorithmThenShouldNotFail() throws IOException { + fakeKnownHosts = knownHostsTestUtil.createFakeKnownHosts("fake2.ssh", "known_hosts_fake2", + "github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg="); + KnownHostsFileVerifier knownHostsFileVerifier = spy(new KnownHostsFileVerifier()); + when(knownHostsFileVerifier.getKnownHostsFile()).thenReturn(fakeKnownHosts); + AbstractJGitHostKeyVerifier verifier = knownHostsFileVerifier.forJGit(TaskListener.NULL); + JGitConnection jGitConnection = new JGitConnection("github.com", 22); + + try { + jGitConnection.connect(verifier); + } catch (IOException e) { + fail("Should not fail because hostkey for 'github.com:22' is in known_hosts with algorithm 'ecdsa-sha2-nistp256'"); + } + } + + @Test + public void testVerifyHostKeyOptionWithDefaultFile() throws Exception { + KnownHostsFileVerifier verifier = new KnownHostsFileVerifier(); + assertThat(verifier.forCliGit(TaskListener.NULL).getVerifyHostKeyOption(null), is("-o StrictHostKeyChecking=yes")); + } + +}
src/test/java/org/jenkinsci/plugins/gitclient/verifier/KnownHostsTestUtil.java+47 −0 added@@ -0,0 +1,47 @@ +package org.jenkinsci.plugins.gitclient.verifier; + +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; + +public class KnownHostsTestUtil { + + private final TemporaryFolder testFolder; + + public KnownHostsTestUtil(TemporaryFolder testFolder) { + this.testFolder = testFolder; + } + + public File createFakeSSHDir(String dir) throws IOException { + // Create a fake directory for use with a known_hosts file + return testFolder.newFolder(dir); + } + + public File createFakeKnownHosts(String dir, String name) throws IOException { + // Create fake known hosts file + File fakeSSHDir = createFakeSSHDir(dir); + return new File(fakeSSHDir, name); + } + + public File createFakeKnownHosts(String dir, String name, String fileContent) throws IOException { + File fakeKnownHosts = createFakeKnownHosts(dir , name); + byte[] fakeKnownHostsBytes = fileContent.getBytes(StandardCharsets.UTF_8); + Files.write(fakeKnownHosts.toPath(), fakeKnownHostsBytes); + return fakeKnownHosts; + } + + public File createFakeKnownHosts(String fileContent) throws IOException { + File fakeKnownHosts = createFakeKnownHosts("fake.ssh", "known_hosts_fake"); + byte[] fakeKnownHostsBytes = fileContent.getBytes(StandardCharsets.UTF_8); + Files.write(fakeKnownHosts.toPath(), fakeKnownHostsBytes); + return fakeKnownHosts; + } + + public List<String> getKnownHostsContent(File file) throws IOException { + return Files.readAllLines(file.toPath()); + } +}
src/test/java/org/jenkinsci/plugins/gitclient/verifier/ManuallyProvidedKeyVerifierTest.java+143 −0 added@@ -0,0 +1,143 @@ +package org.jenkinsci.plugins.gitclient.verifier; + +import hudson.Functions; +import hudson.model.TaskListener; +import org.jenkinsci.plugins.gitclient.trilead.JGitConnection; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Collections; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.fail; + +@RunWith(MockitoJUnitRunner.class) +public class ManuallyProvidedKeyVerifierTest { + + // Create a temporary folder and assert folder deletion at end of tests + @Rule + public TemporaryFolder testFolder = TemporaryFolder.builder().assureDeletion().build(); + + private AbstractJGitHostKeyVerifier verifier; + private String hostKey; + + @Before + public void assignVerifier() { + hostKey = "github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"; + } + + @Test + public void connectWhenHostKeyProvidedForOtherHostNameThenShouldFail() { + verifier = new ManuallyProvidedKeyVerifier(hostKey).forJGit(TaskListener.NULL); + JGitConnection jGitConnection = new JGitConnection("bitbucket.org", 22); + + try { + jGitConnection.connect(verifier); + fail("Should fail because hostkey for 'bitbucket.org:22' is not manually provided"); + } catch (IOException e) { + assertThat(e.getMessage(), is("There was a problem while connecting to bitbucket.org:22")); + } + } + + @Test + public void connectWhenHostKeyProvidedThenShouldNotFail() { + AbstractJGitHostKeyVerifier verifier = new ManuallyProvidedKeyVerifier(hostKey).forJGit(TaskListener.NULL); + JGitConnection jGitConnection = new JGitConnection("github.com", 22); + + try { + jGitConnection.connect(verifier); + } catch (IOException e) { + fail("Should not fail because hostkey for 'github.com:22' was provided"); + } + } + + @Test + public void connectWhenWrongHostKeyProvidedThenShouldFail() { + verifier = new ManuallyProvidedKeyVerifier("github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9OOOO").forJGit(TaskListener.NULL); + JGitConnection jGitConnection = new JGitConnection("github.com", 22); + + try { + jGitConnection.connect(verifier); + fail("Should fail because hostkey for 'github.com' is wrong"); + } catch (IOException e) { + assertThat(e.getMessage(), is("There was a problem while connecting to github.com:22")); + } + } + + @Test + public void connectWhenHostKeyProvidedWithPortThenShouldNotFail() { + AbstractJGitHostKeyVerifier verifier = new ManuallyProvidedKeyVerifier("github.com:22 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl").forJGit(TaskListener.NULL); + JGitConnection jGitConnection = new JGitConnection("github.com", 22); + + try { + jGitConnection.connect(verifier); + } catch (IOException e) { + fail("Should not fail because hostkey for 'github.com:22' was provided, but fails with: " + e.getMessage()); + } + } + + @Test + public void connectWhenProvidedHostnameWithPortHashedShouldNotFail() { + // |1|L95XQhkJWMDrDLdtkT1oH7hj2ec=|A2ocjuIDw2x+SOhTnRU3IGjqai0= is github.com:22 + AbstractJGitHostKeyVerifier verifier = new ManuallyProvidedKeyVerifier("|1|L95XQhkJWMDrDLdtkT1oH7hj2ec=|A2ocjuIDw2x+SOhTnRU3IGjqai0= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=").forJGit(TaskListener.NULL); + JGitConnection jGitConnection = new JGitConnection("github.com", 22); + + try { + jGitConnection.connect(verifier); + } catch (IOException e) { + fail("Should not fail because hostkey for 'github.com:22' was provided, but fails with: " + e.getMessage()); + } + } + + @Test + public void connectWhenProvidedHostnameWithoutPortHashedShouldNotFail() { + // |1|Sps9q6AJcYKtFor8T+uOUSdidVc=|liZf9T3FN9jJG2NPwUXK9b/YB+g= is github.com + AbstractJGitHostKeyVerifier verifier = new ManuallyProvidedKeyVerifier("|1|Sps9q6AJcYKtFor8T+uOUSdidVc=|liZf9T3FN9jJG2NPwUXK9b/YB+g= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=").forJGit(TaskListener.NULL); + JGitConnection jGitConnection = new JGitConnection("github.com", 22); + + try { + jGitConnection.connect(verifier); + } catch (IOException e) { + fail("Should not fail because hostkey for 'github.com' was provided, but fails with: " + e.getMessage()); + } + } + @Test + public void connectWhenHostKeyProvidedThenShouldFail() { + verifier = new ManuallyProvidedKeyVerifier("github.com:33 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl").forJGit(TaskListener.NULL); + JGitConnection jGitConnection = new JGitConnection("github.com", 22); + + try { + jGitConnection.connect(verifier); + fail("Should fail because hostkey for 'github.com:33' was provided instead of 'github.com:22'"); + } catch (IOException e) { + assertThat(e.getMessage(), is("There was a problem while connecting to github.com:22")); + } + } + @Test + public void testGetVerifyHostKeyOption() throws IOException { + Assume.assumeFalse("test can not run on windows", Functions.isWindows()); + File tempFile = testFolder.newFile(); + String actual = new ManuallyProvidedKeyVerifier(hostKey).forCliGit(TaskListener.NULL).getVerifyHostKeyOption(tempFile); + assertThat(actual, is("-o StrictHostKeyChecking=yes -o UserKnownHostsFile=\\\"\"\"" + tempFile.getAbsolutePath() + "\\\"\"\"")); + assertThat(Files.readAllLines(tempFile.toPath()), is(Collections.singletonList(hostKey))); + } + + @Test + public void testGetVerifyHostKeyOptionOnWindows() throws IOException { + Assume.assumeTrue("test should run on windows", Functions.isWindows()); + File tempFile = testFolder.newFile(); + String actual = new ManuallyProvidedKeyVerifier(hostKey).forCliGit(TaskListener.NULL).getVerifyHostKeyOption(tempFile); + assertThat(actual, is("-o StrictHostKeyChecking=yes -o UserKnownHostsFile=" + tempFile.getAbsolutePath() + "")); + assertThat(Files.readAllLines(tempFile.toPath()), is(Collections.singletonList(hostKey))); + } + +}
src/test/java/org/jenkinsci/plugins/gitclient/verifier/NoHostKeyVerifierTest.java+38 −0 added@@ -0,0 +1,38 @@ +package org.jenkinsci.plugins.gitclient.verifier; + +import hudson.model.TaskListener; +import org.jenkinsci.plugins.gitclient.trilead.JGitConnection; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.fail; + +public class NoHostKeyVerifierTest { + + private NoHostKeyVerifier verifier; + + @Before + public void assignVerifier() { + verifier = new NoHostKeyVerifier(); + } + + @Test + public void testVerifyServerHostKey() { + JGitConnection jGitConnection = new JGitConnection("github.com", 22); + try { + jGitConnection.connect(verifier.forJGit(TaskListener.NULL)); + } catch (IOException e) { + fail("Should not fail because verifyServerHostKey always true"); + } + } + + @Test + public void testVerifyHostKeyOption() throws IOException { + assertThat(verifier.forCliGit(TaskListener.NULL).getVerifyHostKeyOption(new File("")), is("-o StrictHostKeyChecking=no")); + } +}
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-cm7j-p8hc-97vjghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-36881ghsaADVISORY
- www.openwall.com/lists/oss-security/2022/07/27/1ghsamailing-listx_refsource_MLISTWEB
- github.com/jenkinsci/git-client-plugin/commit/88f52c6c9b18bca4ad210e3b9910a49433583fd9ghsaWEB
- www.jenkins.io/security/advisory/2022-07-27/ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.