CVE-2023-37943
Description
Jenkins Active Directory Plugin 2.30 and earlier ignores the "Require TLS" and "StartTls" options and always performs the connection test to Active directory unencrypted, allowing attackers able to capture network traffic between the Jenkins controller and Active Directory servers to obtain Active Directory credentials.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Jenkins Active Directory Plugin before 2.30.1 transmits AD credentials in plaintext during connection test, allowing network capture.
CVE-2023-37943 affects the Jenkins Active Directory Plugin versions 2.30 and earlier. The plugin's connection test feature, triggered by the "Test Domain" button, ignores the configured "Require TLS" and "StartTls" options and always performs the connection to Active Directory over an unencrypted channel [1][3]. This means that even if TLS is enabled for actual login operations, the test connection sends credentials in plaintext.
An attacker with the ability to capture network traffic between the Jenkins controller and the Active Directory server can intercept this unencrypted connection test. The attack requires network access to the communication path but does not require any authentication to Jenkins [1][2]. The credentials used for the test are typically those of a privileged Active Directory user, making them highly valuable.
Successful exploitation allows the attacker to obtain Active Directory credentials, which can then be used to authenticate to the AD domain and potentially access other resources protected by AD [3]. The impact is considered low severity because it only affects the connection test, not the production login flow, but credential exposure is still significant.
The vulnerability is fixed in Active Directory Plugin version 2.30.1, which properly respects the TLS settings during connection tests [1][4]. Users should upgrade to this version or later to mitigate the risk. No workarounds are mentioned, but ensuring network segmentation and monitoring can reduce exposure.
AI Insight generated on May 20, 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:active-directoryMaven | < 2.30.1 | 2.30.1 |
Affected products
2- Jenkins Project/Jenkins Active Directory Pluginv5Range: 0
Patches
1549dde617dbc[SECURITY-3059]
12 files changed · +82 −45
pom.xml+1 −1 modified@@ -34,7 +34,7 @@ </developers> <properties> - <revision>2.31</revision> + <revision>2.30.1</revision> <changelist>-SNAPSHOT</changelist> <gitHubRepo>jenkinsci/${project.artifactId}-plugin</gitHubRepo> <jenkins.version>2.332.4</jenkins.version>
src/main/java/hudson/plugins/active_directory/ActiveDirectoryDomain.java+5 −4 modified@@ -268,14 +268,15 @@ public ListBoxModel doFillTlsConfigurationItems() { @RequirePOST public FormValidation doValidateTest(@QueryParameter(fixEmpty = true) String name, @QueryParameter(fixEmpty = true) String servers, @QueryParameter(fixEmpty = true) String site, @QueryParameter(fixEmpty = true) String bindName, - @QueryParameter(fixEmpty = true) String bindPassword, @QueryParameter(fixEmpty = true) TlsConfiguration tlsConfiguration, @QueryParameter(fixEmpty = true) boolean requireTLS) throws IOException, ServletException, NamingException { + @QueryParameter(fixEmpty = true) String bindPassword, @QueryParameter(fixEmpty = true) TlsConfiguration tlsConfiguration, @QueryParameter GroupLookupStrategy groupLookupStrategy, + @QueryParameter(fixEmpty = false) boolean removeIrrelevantGroups, @QueryParameter(fixEmpty = true) boolean startTls, @QueryParameter(fixEmpty = true) boolean requireTLS) throws NamingException { Jenkins.get().checkPermission(Jenkins.ADMINISTER); ActiveDirectoryDomain domain = new ActiveDirectoryDomain(name, servers, site, bindName, bindPassword, tlsConfiguration); List<ActiveDirectoryDomain> domains = new ArrayList<>(1); domains.add(domain); - ActiveDirectorySecurityRealm activeDirectorySecurityRealm = new ActiveDirectorySecurityRealm(null, domains, site, bindName, - bindPassword, null, GroupLookupStrategy.AUTO, false, true, null, false, (ActiveDirectoryInternalUsersDatabase) null, requireTLS); + ActiveDirectorySecurityRealm activeDirectorySecurityRealm = new ActiveDirectorySecurityRealm(name, domains, site, bindName, + bindPassword, null, groupLookupStrategy, removeIrrelevantGroups, true, null, startTls, null, requireTLS); ClassLoader ccl = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); @@ -325,7 +326,7 @@ public FormValidation doValidateTest(@QueryParameter(fixEmpty = true) String nam // Make sure the bind actually works try { Hashtable<String, String> props = new Hashtable<>(0); - DirContext context = activeDirectorySecurityRealm.getDescriptor().bind(bindName, Secret.toString(password), obtainerServers, props, tlsConfiguration, requireTLS); + DirContext context = activeDirectorySecurityRealm.getDescriptor().bind(bindName, Secret.toString(password), obtainerServers, props, tlsConfiguration, requireTLS, startTls); try { // Actually do a search to make sure the credential is valid Attributes userAttributes = new LDAPSearchBuilder(context, toDC(name)).subTreeScope().searchOne("(objectClass=user)");
src/main/java/hudson/plugins/active_directory/ActiveDirectorySecurityRealm.java+30 −15 modified@@ -567,13 +567,18 @@ public DirContext bind(String principalName, String password, List<SocketInfo> l return bind(principalName, password, ldapServers, props, tlsConfiguration, isRequireTLS()); } - /** - * Binds to the server using the specified username/password. - * <p> - * In a real deployment, often there are servers that don't respond or - * otherwise broken, so try all the servers. - */ + @Deprecated public DirContext bind(String principalName, String password, List<SocketInfo> ldapServers, Hashtable<String, String> props, TlsConfiguration tlsConfiguration, boolean requireTLS) throws NamingException { + return bind(principalName, password, ldapServers, props, tlsConfiguration, requireTLS, isStartTLS()); + } + + /** + * Binds to the server using the specified username/password. + * <p> + * In a real deployment, often there are servers that don't respond or + * otherwise broken, so try all the servers. + */ + public DirContext bind(String principalName, String password, List<SocketInfo> ldapServers, Hashtable<String, String> props, TlsConfiguration tlsConfiguration, boolean requireTLS, boolean startTls) throws NamingException { // in a AD forest, it'd be mighty nice to be able to login as "joe" // as opposed to "joe@europe", // but the bind operation doesn't appear to allow me to do so. @@ -599,7 +604,7 @@ public DirContext bind(String principalName, String password, List<SocketInfo> l for (SocketInfo ldapServer : ldapServers) { try { - LdapContext context = bind(principalName, password, ldapServer, newProps, tlsConfiguration, requireTLS); + LdapContext context = bind(principalName, password, ldapServer, newProps, tlsConfiguration, requireTLS, startTls); LOGGER.fine("Bound to " + ldapServer); return context; } catch (javax.naming.AuthenticationException e) { @@ -656,8 +661,15 @@ private LdapContext bind(String principalName, String password, SocketInfo serve return bind(principalName, password, server, props, null, isRequireTLS()); } + @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD", justification = "Deprecated method.It will removed at some point") @IgnoreJRERequirement + @Deprecated private LdapContext bind(String principalName, String password, SocketInfo server, Hashtable<String, String> props, TlsConfiguration tlsConfiguration, boolean requireTLS) throws NamingException { + return bind(principalName, password, server, props, tlsConfiguration, requireTLS, isStartTLS()); + } + + @IgnoreJRERequirement + private LdapContext bind(String principalName, String password, SocketInfo server, Hashtable<String, String> props, TlsConfiguration tlsConfiguration, boolean requireTLS, boolean startTLS) throws NamingException { String ldapUrl = (requireTLS?"ldaps://":"ldap://") + server + '/'; String oldName = Thread.currentThread().getName(); Thread.currentThread().setName("Connecting to "+ldapUrl+" : "+oldName); @@ -670,14 +682,7 @@ private LdapContext bind(String principalName, String password, SocketInfo serve LdapContext context = new InitialLdapContext(props, null); - boolean isStartTls = true; - SecurityRealm securityRealm = Jenkins.getActiveInstance().getSecurityRealm(); - if (securityRealm instanceof ActiveDirectorySecurityRealm) { - ActiveDirectorySecurityRealm activeDirectorySecurityRealm = (ActiveDirectorySecurityRealm) securityRealm; - isStartTls= activeDirectorySecurityRealm.isStartTls(); - } - - if (!requireTLS && isStartTls) { + if (!requireTLS && startTLS) { // try to upgrade to TLS if we can, but failing to do so isn't fatal // see http://download.oracle.com/javase/jndi/tutorial/ldap/ext/starttls.html StartTlsResponse rsp = null; @@ -858,6 +863,16 @@ private boolean isRequireTLS() { } return requireTLS; } + + @Deprecated // this is here purely to ease access to the variable from the descriptor. + private boolean isStartTLS() { + boolean startTLS = true; // secure by default + SecurityRealm securityRealm = Jenkins.get().getSecurityRealm(); + if (securityRealm instanceof ActiveDirectorySecurityRealm) { + startTLS = Boolean.TRUE.equals(((ActiveDirectorySecurityRealm)securityRealm).isStartTls()); + } + return startTLS; + } } @Override
src/main/java/hudson/plugins/active_directory/TlsConfiguration.java+1 −1 modified@@ -6,7 +6,7 @@ * Classification of all possible TLS configurations * */ -enum TlsConfiguration { +public enum TlsConfiguration { TRUST_ALL_CERTIFICATES (Messages._TlsConfiguration_TrustAllCertificates()), JDK_TRUSTSTORE (Messages._TlsConfiguration_JdkTrustStore()) ;
src/main/resources/hudson/plugins/active_directory/ActiveDirectorySecurityRealm/configAdvanced.jelly+3 −3 modified@@ -1,12 +1,12 @@ <?jelly escape-by-default='true'?> <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> <f:advanced> - <f:entry field="startTls" title="${%Enable StartTls}"> - <f:checkbox name="startTls" default="true" /> - </f:entry> <f:entry field="groupLookupStrategy" title="${%Group Membership Lookup Strategy}"> <f:select default="TOKENGROUPS"/> </f:entry> + <f:entry field="startTls" title="${%Enable StartTls}"> + <f:checkbox name="startTls" default="true" /> + </f:entry> <f:optionalBlock field="internalUsersDatabase" title="${%Use Jenkins Internal Database}" checked="${instance.internalUsersDatabase != null}"> <f:entry field="jenkinsInternalUser" title="${%Jenkins Internal Database User}"> <f:textbox />
src/main/resources/hudson/plugins/active_directory/ActiveDirectorySecurityRealm/config.jelly+4 −4 modified@@ -42,6 +42,9 @@ </j:when> <j:otherwise> <!-- code path for ActiveDirectoryUnixAuthenticationProvider: running on Unix --> + <st:include page="configAdvanced.jelly" class="${descriptor.clazz}"/> + <st:include page="configCache.jelly" class="${descriptor.clazz}"/> + <st:include page="requireTLS.jelly" class="${descriptor.clazz}"/> <f:entry field="domains" title="${%Domains}"> <f:repeatable field="domains" add="${%Add Domain}"> <a:blockWrapper> @@ -65,17 +68,14 @@ <f:select /> </f:entry> <f:nested> - <f:validateButton with="name,servers,site,bindName,bindPassword,tlsConfiguration" title="${%Test Domain}" method="validateTest"/> + <f:validateButton with="name,servers,site,bindName,bindPassword,tlsConfiguration,groupLookupStrategy,removeIrrelevantGroups,startTls,requireTLS" title="${%Test Domain}" method="validateTest"/> </f:nested> </a:blockWrapper> <div align="right"> <input type="button" value="Delete Domain" class="repeatable-delete" style="margin-left: 1em;" /> </div> </f:repeatable> </f:entry> - <st:include page="configAdvanced.jelly" class="${descriptor.clazz}"/> - <st:include page="configCache.jelly" class="${descriptor.clazz}"/> - <st:include page="requireTLS.jelly" class="${descriptor.clazz}"/> </j:otherwise> </j:choose> </j:jelly>
src/main/resources/hudson/plugins/active_directory/Security1389AdministrativeMonitorLegacySysProp/message.jelly+1 −1 modified@@ -3,7 +3,7 @@ <div class="alert alert-warning"> <form method="post" action="${rootURL}/${it.url}/disable"> - <f:submit primary="false" clazz="jenkins-!-destructive-color" value="${%Dismiss}"/> + <f:submit value="${%Dismiss}" /> </form> <p>${%blurb}</p> </div>
src/main/resources/hudson/plugins/active_directory/Security1389AdministrativeMonitor/message.jelly+1 −1 modified@@ -3,7 +3,7 @@ <div class="alert alert-danger"> <form method="post" action="${rootURL}/${it.url}/disable"> - <f:submit primary="false" clazz="jenkins-!-destructive-color" value="${%Dismiss}"/> + <f:submit value="${%Dismiss}"/> </form> <p>${%blurb(rootURL + "/configureSecurity/")}</p> </div>
src/test/java/hudson/plugins/active_directory/docker/ActiveDirectoryGenericContainer.java+1 −0 modified@@ -34,6 +34,7 @@ public ActiveDirectoryGenericContainer() { */ public ActiveDirectoryGenericContainer<SELF> withStaticPorts() { addFixedExposedPort(3268, 3268); // global catalog + addFixedExposedPort(3269, 3269); // global catalog over tls addFixedExposedPort(53, 53, InternetProtocol.TCP); // DNS over TCP addFixedExposedPort(53, 53, InternetProtocol.UDP); // DNS over UDP return this;
src/test/java/hudson/plugins/active_directory/docker/TheFlintstonesIT.java+31 −11 modified@@ -30,20 +30,18 @@ import java.util.List; import javax.naming.NamingException; import javax.servlet.ServletException; + +import hudson.plugins.active_directory.TlsConfiguration; import org.acegisecurity.AuthenticationServiceException; import org.acegisecurity.userdetails.UserDetails; -import org.junit.Assume; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.LoggerRule; -import org.testcontainers.DockerClientFactory; import hudson.plugins.active_directory.ActiveDirectoryDomain; import hudson.plugins.active_directory.ActiveDirectorySecurityRealm; import hudson.plugins.active_directory.GroupLookupStrategy; -import hudson.util.Secret; /** * Integration tests with Docker and requiring custom DNS in the target env with fixed ports. @@ -66,16 +64,22 @@ public class TheFlintstonesIT { public final static String AD_MANAGER_DN = "CN=Administrator,CN=Users,DC=SAMDOM,DC=EXAMPLE,DC=COM"; public final static String AD_MANAGER_DN_PASSWORD = "ia4uV1EeKait"; public final static int MAX_RETRIES = 30; + public final static int GLOBAL_CATALOG_PLAIN_TEXT = 3268; + public final static int GLOBAL_CATALOG_TLS = 3269; public String dockerIp; public int dockerPort; public void dynamicSetUp() throws Exception { - dockerIp = docker.getHost(); - dockerPort = docker.getMappedPort(3268); + dynamicSetUp(false); + } + + public void dynamicSetUp(boolean requireTLS) throws Exception { + dockerIp = requireTLS ? docker.getHost() : "dc1.samdom.example.com"; + dockerPort = docker.getMappedPort(requireTLS ? GLOBAL_CATALOG_TLS : GLOBAL_CATALOG_PLAIN_TEXT); ActiveDirectoryDomain activeDirectoryDomain = new ActiveDirectoryDomain(AD_DOMAIN, dockerIp + ":" + dockerPort , null, AD_MANAGER_DN, AD_MANAGER_DN_PASSWORD); List<ActiveDirectoryDomain> domains = new ArrayList<>(1); domains.add(activeDirectoryDomain); - ActiveDirectorySecurityRealm activeDirectorySecurityRealm = new ActiveDirectorySecurityRealm(null, domains, null, null, null, null, GroupLookupStrategy.RECURSIVE, false, true, null, false, null, false); + ActiveDirectorySecurityRealm activeDirectorySecurityRealm = new ActiveDirectorySecurityRealm(null, domains, null, null, null, null, GroupLookupStrategy.RECURSIVE, false, true, null, false, null, requireTLS); j.getInstance().setSecurityRealm(activeDirectorySecurityRealm); while(!docker.getLogs().contains("custom (exit status 0; expected)")) { Thread.sleep(1000); @@ -97,15 +101,15 @@ public void dynamicSetUp() throws Exception { public void validateCustomDomainController() throws ServletException, NamingException, IOException, Exception { dynamicSetUp(); ActiveDirectoryDomain.DescriptorImpl adDescriptor = new ActiveDirectoryDomain.DescriptorImpl(); - assertEquals("OK: Success", adDescriptor.doValidateTest(AD_DOMAIN, dockerIp + ":" + dockerPort, null, AD_MANAGER_DN, AD_MANAGER_DN_PASSWORD, null, false).toString().trim()); + assertEquals("OK: Success", adDescriptor.doValidateTest(AD_DOMAIN, dockerIp + ":" + dockerPort, null, AD_MANAGER_DN, AD_MANAGER_DN_PASSWORD, null, null, false, false, false).toString().trim()); } @Issue("JENKINS-36148") @Test public void validateDomain() throws ServletException, NamingException, IOException, Exception { dynamicSetUp(); ActiveDirectoryDomain.DescriptorImpl adDescriptor = new ActiveDirectoryDomain.DescriptorImpl(); - assertEquals("OK: Success", adDescriptor.doValidateTest(AD_DOMAIN, null, null, AD_MANAGER_DN, AD_MANAGER_DN_PASSWORD, null, false).toString().trim()); + assertEquals("OK: Success", adDescriptor.doValidateTest(AD_DOMAIN, null, null, AD_MANAGER_DN, AD_MANAGER_DN_PASSWORD, null, null, false, false, false).toString().trim()); } @@ -114,15 +118,31 @@ public void validateDomain() throws ServletException, NamingException, IOExcepti public void validateTestDomainRequireTLSDisabled() throws Exception { dynamicSetUp(); ActiveDirectoryDomain.DescriptorImpl adDescriptor = new ActiveDirectoryDomain.DescriptorImpl(); - assertEquals("OK: Success", adDescriptor.doValidateTest(AD_DOMAIN, null, null, AD_MANAGER_DN, AD_MANAGER_DN_PASSWORD, null, false).toString().trim()); + assertEquals("OK: Success", adDescriptor.doValidateTest(AD_DOMAIN, null, null, AD_MANAGER_DN, AD_MANAGER_DN_PASSWORD, null, null, false, false, false).toString().trim()); } @Issue("JENKINS-69683") @Test public void validateTestDomainServerRequireTLSDisabled() throws Exception { dynamicSetUp(); ActiveDirectoryDomain.DescriptorImpl adDescriptor = new ActiveDirectoryDomain.DescriptorImpl(); - assertEquals("OK: Success", adDescriptor.doValidateTest(AD_DOMAIN, dockerIp + ":" + dockerPort, null, AD_MANAGER_DN, AD_MANAGER_DN_PASSWORD, null, false).toString().trim()); + assertEquals("OK: Success", adDescriptor.doValidateTest(AD_DOMAIN, dockerIp + ":" + dockerPort, null, AD_MANAGER_DN, AD_MANAGER_DN_PASSWORD, null, null, false, false, false).toString().trim()); + } + + @Issue("SECURITY-3059") + @Test + public void validateTestDomainServerRequireTLSEnabled() throws Exception { + dynamicSetUp(true); + ActiveDirectoryDomain.DescriptorImpl adDescriptor = new ActiveDirectoryDomain.DescriptorImpl(); + assertEquals("OK: Success", adDescriptor.doValidateTest(AD_DOMAIN, "dc1.samdom.example.com" + ":" + dockerPort, null, AD_MANAGER_DN, AD_MANAGER_DN_PASSWORD, TlsConfiguration.TRUST_ALL_CERTIFICATES, GroupLookupStrategy.TOKENGROUPS, false, false, true).toString().trim()); + } + + @Issue("SECURITY-3059") + @Test + public void validateTestDomainServerRequireStartTLSEnabled() throws Exception { + dynamicSetUp(); + ActiveDirectoryDomain.DescriptorImpl adDescriptor = new ActiveDirectoryDomain.DescriptorImpl(); + assertEquals("OK: Success", adDescriptor.doValidateTest(AD_DOMAIN, "dc1.samdom.example.com" + ":" + dockerPort, null, AD_MANAGER_DN, AD_MANAGER_DN_PASSWORD, TlsConfiguration.TRUST_ALL_CERTIFICATES, GroupLookupStrategy.TOKENGROUPS, false, true, false).toString().trim()); } }
src/test/java/hudson/plugins/active_directory/WindowsAdsiModeUserCacheDisabledIT.java+2 −2 modified@@ -38,14 +38,14 @@ public static void setUp() { public void dynamicCacheEnableSetUp() throws Exception { CacheConfiguration cacheConfiguration = new CacheConfiguration(500,30); ActiveDirectorySecurityRealm activeDirectorySecurityRealm = new ActiveDirectorySecurityRealm(null, null, null, null, - null, null, null, false, null, cacheConfiguration, null, (ActiveDirectoryInternalUsersDatabase) null); + null, null, null, false, null, cacheConfiguration, null, (ActiveDirectoryInternalUsersDatabase) null, false); j.jenkins.setSecurityRealm(activeDirectorySecurityRealm); } public void dynamicCacheDisabledSetUp() throws Exception { ActiveDirectorySecurityRealm activeDirectorySecurityRealm = new ActiveDirectorySecurityRealm(null, null, null, null, - null, null, null, false, null, null, null, (ActiveDirectoryInternalUsersDatabase) null); + null, null, null, false, null, null, null, (ActiveDirectoryInternalUsersDatabase) null, false); j.jenkins.setSecurityRealm(activeDirectorySecurityRealm); }
src/test/java/hudson/plugins/active_directory/WindowsAdsiModeUserCacheEnabledIT.java+2 −2 modified@@ -58,14 +58,14 @@ public static void disableHealthMetrics() { public void dynamicCacheEnableSetUp() throws Exception { CacheConfiguration cacheConfiguration = new CacheConfiguration(500,30); ActiveDirectorySecurityRealm activeDirectorySecurityRealm = new ActiveDirectorySecurityRealm(null, null, null, null, - null, null, null, false, null, cacheConfiguration, null, (ActiveDirectoryInternalUsersDatabase) null); + null, null, null, false, null, cacheConfiguration, null, (ActiveDirectoryInternalUsersDatabase) null, false); j.jenkins.setSecurityRealm(activeDirectorySecurityRealm); } public void dynamicCacheDisabledSetUp() throws Exception { ActiveDirectorySecurityRealm activeDirectorySecurityRealm = new ActiveDirectorySecurityRealm(null, null, null, null, - null, null, null, false, null, null, null, (ActiveDirectoryInternalUsersDatabase) null); + null, null, null, false, null, null, null, (ActiveDirectoryInternalUsersDatabase) null, false); j.jenkins.setSecurityRealm(activeDirectorySecurityRealm); }
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-g8c3-6fj2-87w7ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-37943ghsaADVISORY
- www.jenkins.io/security/advisory/2023-07-12/ghsavendor-advisoryWEB
- www.openwall.com/lists/oss-security/2023/07/12/2ghsaWEB
- github.com/jenkinsci/active-directory-plugin/commit/549dde617dbcf533e6cabfe8cc148a250a398211ghsaWEB
News mentions
1- Jenkins Security Advisory 2023-07-12Jenkins Security Advisories · Jul 12, 2023