Version in base suite: 3.4.13-6 Base version: zookeeper_3.4.13-6 Target version: zookeeper_3.4.13-6+deb11u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/z/zookeeper/zookeeper_3.4.13-6.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/z/zookeeper/zookeeper_3.4.13-6+deb11u1.dsc changelog | 15 ++ patches/CVE-2023-44981.patch | 267 +++++++++++++++++++++++++++++++++++++++++++ patches/series | 1 3 files changed, 283 insertions(+) diff -Nru zookeeper-3.4.13/debian/changelog zookeeper-3.4.13/debian/changelog --- zookeeper-3.4.13/debian/changelog 2021-02-07 22:16:46.000000000 +0000 +++ zookeeper-3.4.13/debian/changelog 2023-10-28 21:16:44.000000000 +0000 @@ -1,3 +1,18 @@ +zookeeper (3.4.13-6+deb11u1) bullseye-security; urgency=medium + + * Team upload: + - CVE-2023-44981: Prevent a potential authorisation bypass vulnerability. + If SASL Quorum Peer authentication was enabled (via + quorum.auth.enableSasl), authorisation was performed by verifying that + the instance part in the SASL authentication ID was listed in the zoo.cfg + server list. However, this value is optional, and, if missing (such as in + 'eve@EXAMPLE.COM'), the authorisation check will be skipped. As a result, + an arbitrary endpoint could join the cluster and begin propagating + counterfeit changes to the leader, essentially giving it complete + read-write access to the data tree. (Closes: #1054224) + + -- Pierre Gruet Sat, 28 Oct 2023 23:16:44 +0200 + zookeeper (3.4.13-6) unstable; urgency=medium * Team upload. diff -Nru zookeeper-3.4.13/debian/patches/CVE-2023-44981.patch zookeeper-3.4.13/debian/patches/CVE-2023-44981.patch --- zookeeper-3.4.13/debian/patches/CVE-2023-44981.patch 1970-01-01 00:00:00.000000000 +0000 +++ zookeeper-3.4.13/debian/patches/CVE-2023-44981.patch 2023-10-28 21:16:44.000000000 +0000 @@ -0,0 +1,267 @@ +From: Damien Diederen +Date: Sun, 24 Sep 2023 14:28:56 +0200 +Subject: [PATCH] ZOOKEEPER-4753: zookeeper-server: Improvement: Explicit + handling of DIGEST-MD5 vs GSSAPI in quorum auth + +Before this, the SASL-based quorum authorizer did not explicitly +distinguish between the DIGEST-MD5 and GSSAPI mechanisms: it was +simply relying on NameCallback and PasswordCallback for authentication +with the former and examining Kerberos principals in AuthorizeCallback +for the latter. + +It turns out that some SASL/DIGEST-MD5 configurations cause +authentication and authorization IDs not to match the expected format, +and the DIGEST-MD5-based portions of the quorum test suite to fail +with obscure errors. (They can be traced to failures to join the +quorum, but only by looking into detailed logs.) + +This patch uses the login module name to determine whether DIGEST-MD5 +or GSSAPI is used, and relaxes the authentication ID check for the +former. As a cleanup, it keeps the password-based credential map +empty when Kerberos principals are expected. It finally adapts a +test, and adds a new one, ensuring weirdly-shaped credentials only +cause authentication failures in the GSSAPI case. +--- + .../auth/SaslQuorumServerCallbackHandler.java | 44 +++++++++---- + .../server/quorum/auth/QuorumKerberosAuthTest.java | 12 ++-- + .../auth/QuorumKerberosHostBasedAuthTest.java | 73 ++++++++++++++++++++-- + 3 files changed, 107 insertions(+), 22 deletions(-) + +diff --git a/src/java/main/org/apache/zookeeper/server/quorum/auth/SaslQuorumServerCallbackHandler.java b/src/java/main/org/apache/zookeeper/server/quorum/auth/SaslQuorumServerCallbackHandler.java +index 3e71bb1..2a8b1a5 100644 +--- a/src/java/main/org/apache/zookeeper/server/quorum/auth/SaslQuorumServerCallbackHandler.java ++++ b/src/java/main/org/apache/zookeeper/server/quorum/auth/SaslQuorumServerCallbackHandler.java +@@ -18,6 +18,7 @@ + package org.apache.zookeeper.server.quorum.auth; + + import java.io.IOException; ++import java.util.Collections; + import java.util.HashMap; + import java.util.Map; + import java.util.Set; +@@ -31,6 +32,7 @@ import javax.security.auth.login.AppConfigurationEntry; + import javax.security.auth.login.Configuration; + import javax.security.sasl.AuthorizeCallback; + import javax.security.sasl.RealmCallback; ++import org.apache.zookeeper.server.auth.DigestLoginModule; + + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; +@@ -46,7 +48,8 @@ public class SaslQuorumServerCallbackHandler implements CallbackHandler { + private static final Logger LOG = LoggerFactory.getLogger(SaslQuorumServerCallbackHandler.class); + + private String userName; +- private final Map credentials = new HashMap(); ++ private final boolean isDigestAuthn; ++ private final Map credentials; + private final Set authzHosts; + + public SaslQuorumServerCallbackHandler(Configuration configuration, +@@ -58,20 +61,35 @@ public class SaslQuorumServerCallbackHandler implements CallbackHandler { + LOG.error(errorMessage); + throw new IOException(errorMessage); + } +- credentials.clear(); ++ ++ Map credentials = new HashMap(); ++ boolean isDigestAuthn = true; ++ + for(AppConfigurationEntry entry: configurationEntries) { +- Map options = entry.getOptions(); +- // Populate DIGEST-MD5 user -> password map with JAAS configuration entries from the "QuorumServer" section. +- // Usernames are distinguished from other options by prefixing the username with a "user_" prefix. +- for(Map.Entry pair : options.entrySet()) { +- String key = pair.getKey(); +- if (key.startsWith(USER_PREFIX)) { +- String userName = key.substring(USER_PREFIX.length()); +- credentials.put(userName,(String)pair.getValue()); ++ if (entry.getLoginModuleName().equals(DigestLoginModule.class.getName())) { ++ Map options = entry.getOptions(); ++ // Populate DIGEST-MD5 user -> password map with JAAS configuration entries from the "QuorumServer" section. ++ // Usernames are distinguished from other options by prefixing the username with a "user_" prefix. ++ for(Map.Entry pair : options.entrySet()) { ++ String key = pair.getKey(); ++ if (key.startsWith(USER_PREFIX)) { ++ String userName = key.substring(USER_PREFIX.length()); ++ credentials.put(userName,(String)pair.getValue()); ++ } + } ++ } else { ++ isDigestAuthn = false; + } + } + ++ this.isDigestAuthn = isDigestAuthn; ++ if (isDigestAuthn) { ++ this.credentials = Collections.unmodifiableMap(credentials); ++ LOG.warn("Using DIGEST-MD5 for quorum authorization"); ++ } else { ++ this.credentials = Collections.emptyMap(); ++ } ++ + // authorized host lists + this.authzHosts = authzHosts; + } +@@ -125,14 +143,16 @@ public class SaslQuorumServerCallbackHandler implements CallbackHandler { + // 2. Verify whether the connecting host is present in authorized hosts. + // If not exists, then connecting peer is not authorized to join the + // ensemble and will reject it. +- if (authzFlag) { ++ if (!isDigestAuthn && authzFlag) { + String[] components = authorizationID.split("[/@]"); + if (components.length == 3) { + authzFlag = authzHosts.contains(components[1]); ++ } else { ++ authzFlag = false; + } + if (!authzFlag) { + LOG.error("SASL authorization completed, {} is not authorized to connect", +- components[1]); ++ authorizationID); + } + } + +diff --git a/src/java/test/org/apache/zookeeper/server/quorum/auth/QuorumKerberosAuthTest.java b/src/java/test/org/apache/zookeeper/server/quorum/auth/QuorumKerberosAuthTest.java +index 65d30db..c2c3fe5 100644 +--- a/src/java/test/org/apache/zookeeper/server/quorum/auth/QuorumKerberosAuthTest.java ++++ b/src/java/test/org/apache/zookeeper/server/quorum/auth/QuorumKerberosAuthTest.java +@@ -50,7 +50,7 @@ public class QuorumKerberosAuthTest extends KerberosSecurityTestcase { + + " debug=true\n" + + " doNotPrompt=true\n" + + " refreshKrb5Config=true\n" +- + " principal=\"" + KerberosTestUtils.getServerPrincipal() + "\";\n" + "};\n" ++ + " principal=\"" + KerberosTestUtils.replaceHostPattern(KerberosTestUtils.getHostServerPrincipal()) + "\";\n" + "};\n" + + "QuorumLearner {\n" + + " com.sun.security.auth.module.Krb5LoginModule required\n" + + " useKeyTab=true\n" +@@ -61,7 +61,7 @@ public class QuorumKerberosAuthTest extends KerberosSecurityTestcase { + + " doNotPrompt=true\n" + + " refreshKrb5Config=true\n" + + " isInitiator=true\n" +- + " principal=\"" + KerberosTestUtils.getLearnerPrincipal() + "\";\n" + "};\n"); ++ + " principal=\"" + KerberosTestUtils.replaceHostPattern(KerberosTestUtils.getHostLearnerPrincipal()) + "\";\n" + "};\n"); + setupJaasConfig(jaasEntries); + } + +@@ -69,10 +69,10 @@ public class QuorumKerberosAuthTest extends KerberosSecurityTestcase { + public void setUp() throws Exception { + // create keytab + keytabFile = new File(KerberosTestUtils.getKeytabFile()); +- String learnerPrincipal = KerberosTestUtils.getLearnerPrincipal(); +- String serverPrincipal = KerberosTestUtils.getServerPrincipal(); +- learnerPrincipal = learnerPrincipal.substring(0, learnerPrincipal.lastIndexOf("@")); +- serverPrincipal = serverPrincipal.substring(0, serverPrincipal.lastIndexOf("@")); ++ String learnerPrincipal = KerberosTestUtils.getHostLearnerPrincipal(); ++ String serverPrincipal = KerberosTestUtils.getHostServerPrincipal(); ++ learnerPrincipal = KerberosTestUtils.replaceHostPattern(learnerPrincipal.substring(0, learnerPrincipal.lastIndexOf("@"))); ++ serverPrincipal = KerberosTestUtils.replaceHostPattern(serverPrincipal.substring(0, serverPrincipal.lastIndexOf("@"))); + getKdc().createPrincipal(keytabFile, learnerPrincipal, serverPrincipal); + } + +diff --git a/src/java/test/org/apache/zookeeper/server/quorum/auth/QuorumKerberosHostBasedAuthTest.java b/src/java/test/org/apache/zookeeper/server/quorum/auth/QuorumKerberosHostBasedAuthTest.java +index 1b5e178..f5e395e 100644 +--- a/src/java/test/org/apache/zookeeper/server/quorum/auth/QuorumKerberosHostBasedAuthTest.java ++++ b/src/java/test/org/apache/zookeeper/server/quorum/auth/QuorumKerberosHostBasedAuthTest.java +@@ -45,12 +45,13 @@ public class QuorumKerberosHostBasedAuthTest extends KerberosSecurityTestcase { + private static String hostServerPrincipal = KerberosTestUtils.getHostServerPrincipal(); + private static String hostLearnerPrincipal = KerberosTestUtils.getHostLearnerPrincipal(); + private static String hostNamedLearnerPrincipal = KerberosTestUtils.getHostNamedLearnerPrincipal("myhost"); ++ private static String hostlessLearnerPrincipal = KerberosTestUtils.getLearnerPrincipal(); + static { +- setupJaasConfigEntries(hostServerPrincipal, hostLearnerPrincipal, hostNamedLearnerPrincipal); ++ setupJaasConfigEntries(hostServerPrincipal, hostLearnerPrincipal, hostNamedLearnerPrincipal, hostlessLearnerPrincipal); + } + + private static void setupJaasConfigEntries(String hostServerPrincipal, +- String hostLearnerPrincipal, String hostNamedLearnerPrincipal) { ++ String hostLearnerPrincipal, String hostNamedLearnerPrincipal, String hostlessLearnerPrincipal) { + String keytabFilePath = FilenameUtils.normalize(KerberosTestUtils.getKeytabFile(), true); + String jaasEntries = new String("" + + "QuorumServer {\n" +@@ -84,7 +85,20 @@ public class QuorumKerberosHostBasedAuthTest extends KerberosSecurityTestcase { + + " doNotPrompt=true\n" + + " refreshKrb5Config=true\n" + + " isInitiator=true\n" +- + " principal=\"" + hostNamedLearnerPrincipal + "\";\n" + "};\n"); ++ + " principal=\"" + hostNamedLearnerPrincipal + "\";\n" + ++ + "};\n" ++ + "QuorumLearnerMissingHost {\n" ++ + " com.sun.security.auth.module.Krb5LoginModule required\n" ++ + " useKeyTab=true\n" ++ + " keyTab=\"" + keytabFilePath ++ + "\"\n" ++ + " storeKey=true\n" ++ + " useTicketCache=false\n" ++ + " debug=false\n" ++ + " refreshKrb5Config=true\n" ++ + " principal=\"" + hostlessLearnerPrincipal ++ + "\";\n" ++ + "};\n"); + setupJaasConfig(jaasEntries); + } + +@@ -101,7 +115,11 @@ public class QuorumKerberosHostBasedAuthTest extends KerberosSecurityTestcase { + + // learner with ipaddress in principal + String learnerPrincipal2 = hostNamedLearnerPrincipal.substring(0, hostNamedLearnerPrincipal.lastIndexOf("@")); +- getKdc().createPrincipal(keytabFile, learnerPrincipal, learnerPrincipal2, serverPrincipal); ++ ++ // learner without host in principal ++ String learnerPrincipal3 = hostlessLearnerPrincipal.substring(0, hostlessLearnerPrincipal.lastIndexOf("@")); ++ ++ getKdc().createPrincipal(keytabFile, learnerPrincipal, learnerPrincipal2, learnerPrincipal3, serverPrincipal); + } + + @After +@@ -192,4 +210,51 @@ public class QuorumKerberosHostBasedAuthTest extends KerberosSecurityTestcase { + badServer.deleteBaseDir(); + } + } ++ /** ++ * Test to verify that the bad server connection to the quorum should be rejected. ++ */ ++ @Test ++ @Timeout(value = 120) ++ public void testConnectHostlessPrincipalBadServer() throws Exception { ++ String serverPrincipal = hostServerPrincipal.substring(0, hostServerPrincipal.lastIndexOf("@")); ++ Map authConfigs = new HashMap<>(); ++ authConfigs.put(QuorumAuth.QUORUM_SASL_AUTH_ENABLED, "true"); ++ authConfigs.put(QuorumAuth.QUORUM_SERVER_SASL_AUTH_REQUIRED, "true"); ++ authConfigs.put(QuorumAuth.QUORUM_LEARNER_SASL_AUTH_REQUIRED, "true"); ++ authConfigs.put(QuorumAuth.QUORUM_KERBEROS_SERVICE_PRINCIPAL, serverPrincipal); ++ String connectStr = startQuorum(3, authConfigs, 3); ++ CountdownWatcher watcher = new CountdownWatcher(); ++ ZooKeeper zk = new ZooKeeper(connectStr, ClientBase.CONNECTION_TIMEOUT, watcher); ++ watcher.waitForConnected(ClientBase.CONNECTION_TIMEOUT); ++ for (int i = 0; i < 10; i++) { ++ zk.create("/" + i, new byte[0], Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); ++ } ++ zk.close(); ++ ++ String quorumCfgSection = mt.get(0).getQuorumCfgSection(); ++ StringBuilder sb = new StringBuilder(); ++ sb.append(quorumCfgSection); ++ ++ int myid = mt.size() + 1; ++ final int clientPort = PortAssignment.unique(); ++ String server = String.format("server.%d=localhost:%d:%d:participant", myid, PortAssignment.unique(), PortAssignment.unique()); ++ sb.append(server + "\n"); ++ quorumCfgSection = sb.toString(); ++ authConfigs.put(QuorumAuth.QUORUM_LEARNER_SASL_LOGIN_CONTEXT, "QuorumLearnerMissingHost"); ++ MainThread badServer = new MainThread(myid, clientPort, quorumCfgSection, authConfigs); ++ badServer.start(); ++ watcher = new CountdownWatcher(); ++ connectStr = "127.0.0.1:" + clientPort; ++ zk = new ZooKeeper(connectStr, ClientBase.CONNECTION_TIMEOUT, watcher); ++ try { ++ watcher.waitForConnected(ClientBase.CONNECTION_TIMEOUT / 3); ++ fail("Must throw exception as the principal does not include an authorized host!"); ++ } catch (TimeoutException e) { ++ // expected ++ } finally { ++ zk.close(); ++ badServer.shutdown(); ++ badServer.deleteBaseDir(); ++ } ++ } + } diff -Nru zookeeper-3.4.13/debian/patches/series zookeeper-3.4.13/debian/patches/series --- zookeeper-3.4.13/debian/patches/series 2021-02-07 22:11:24.000000000 +0000 +++ zookeeper-3.4.13/debian/patches/series 2023-10-28 21:16:44.000000000 +0000 @@ -14,3 +14,4 @@ 16-ZOOKEEPER-1392.patch 17-gcc9-ftbfs-925869.patch 18-java17-compatibility.patch +CVE-2023-44981.patch