From 7fe72d62fcb9246be792b946e405e1d40d402780 Mon Sep 17 00:00:00 2001
From: Ralph Goers <rgoers@apache.org>
Date: Sat, 4 Dec 2021 20:59:39 -0700
Subject: [PATCH] LOG4J2-3201 - Limit the protocols JNDI can use by default.
 Limit the servers and classes that can be accessed via LDAP.

---
 .../logging/log4j/core/net/JndiManager.java   | 62 ++++++++++---------
 .../logging/log4j/core/util/NetUtils.java     |  4 ++
 src/changes/changes.xml                       |  3 +
 src/site/xdoc/manual/appenders.xml            | 27 ++++++++
 src/site/xdoc/manual/configuration.xml.vm     | 26 ++++++++
 src/site/xdoc/manual/extending.xml            |  5 +-
 src/site/xdoc/manual/lookups.xml              |  7 +++
 7 files changed, 103 insertions(+), 31 deletions(-)

diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/net/JndiManager.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/net/JndiManager.java
index b392b93..2d7604f 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/net/JndiManager.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/net/JndiManager.java
@@ -209,43 +209,45 @@ public class JndiManager extends AbstractManager {
     public synchronized <T> T lookup(final String name) throws NamingException {
         try {
             URI uri = new URI(name);
-            if (!allowedProtocols.contains(uri.getScheme().toLowerCase(Locale.ROOT))) {
-                LOGGER.warn("Log4j JNDI does not allow protocol {}", uri.getScheme());
-                return null;
-            }
-            if (LDAP.equalsIgnoreCase(uri.getScheme()) || LDAPS.equalsIgnoreCase(uri.getScheme())) {
-                if (!allowedHosts.contains(uri.getHost())) {
-                    LOGGER.warn("Attempt to access ldap server not in allowed list");
+            if (uri.getScheme() != null) {
+                if (!allowedProtocols.contains(uri.getScheme().toLowerCase(Locale.ROOT))) {
+                    LOGGER.warn("Log4j JNDI does not allow protocol {}", uri.getScheme());
                     return null;
                 }
-                Attributes attributes = this.context.getAttributes(name);
-                if (attributes != null) {
-                    // In testing the "key" for attributes seems to be lowercase while the attribute id is
-                    // camelcase, but that may just be true for the test LDAP used here. This copies the Attributes
-                    // to a Map ignoring the "key" and using the Attribute's id as the key in the Map so it matches
-                    // the Java schema.
-                    Map<String, Attribute> attributeMap = new HashMap<>();
-                    NamingEnumeration<? extends Attribute> enumeration = attributes.getAll();
-                    while (enumeration.hasMore()) {
-                        Attribute attribute = enumeration.next();
-                        attributeMap.put(attribute.getID(), attribute);
+                if (LDAP.equalsIgnoreCase(uri.getScheme()) || LDAPS.equalsIgnoreCase(uri.getScheme())) {
+                    if (!allowedHosts.contains(uri.getHost())) {
+                        LOGGER.warn("Attempt to access ldap server not in allowed list");
+                        return null;
                     }
-                    Attribute classNameAttr = attributeMap.get(CLASS_NAME);
-                    if (attributeMap.get(SERIALIZED_DATA) != null) {
-                        if (classNameAttr != null) {
-                            String className = classNameAttr.get().toString();
-                            if (!allowedClasses.contains(className)) {
-                                LOGGER.warn("Deserialization of {} is not allowed", className);
+                    Attributes attributes = this.context.getAttributes(name);
+                    if (attributes != null) {
+                        // In testing the "key" for attributes seems to be lowercase while the attribute id is
+                        // camelcase, but that may just be true for the test LDAP used here. This copies the Attributes
+                        // to a Map ignoring the "key" and using the Attribute's id as the key in the Map so it matches
+                        // the Java schema.
+                        Map<String, Attribute> attributeMap = new HashMap<>();
+                        NamingEnumeration<? extends Attribute> enumeration = attributes.getAll();
+                        while (enumeration.hasMore()) {
+                            Attribute attribute = enumeration.next();
+                            attributeMap.put(attribute.getID(), attribute);
+                        }
+                        Attribute classNameAttr = attributeMap.get(CLASS_NAME);
+                        if (attributeMap.get(SERIALIZED_DATA) != null) {
+                            if (classNameAttr != null) {
+                                String className = classNameAttr.get().toString();
+                                if (!allowedClasses.contains(className)) {
+                                    LOGGER.warn("Deserialization of {} is not allowed", className);
+                                    return null;
+                                }
+                            } else {
+                                LOGGER.warn("No class name provided for {}", name);
                                 return null;
                             }
-                        } else {
-                            LOGGER.warn("No class name provided for {}", name);
+                        } else if (attributeMap.get(REFERENCE_ADDRESS) != null
+                                || attributeMap.get(OBJECT_FACTORY) != null) {
+                            LOGGER.warn("Referenceable class is not allowed for {}", name);
                             return null;
                         }
-                    } else if (attributeMap.get(REFERENCE_ADDRESS) != null
-                            || attributeMap.get(OBJECT_FACTORY) != null) {
-                        LOGGER.warn("Referenceable class is not allowed for {}", name);
-                        return null;
                     }
                 }
             }
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/NetUtils.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/NetUtils.java
index b9a01ae..6adac59 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/NetUtils.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/NetUtils.java
@@ -83,6 +83,10 @@ public final class NetUtils {
         }
     }
 
+    /**
+     * Returns all the local host names and ip addresses.
+     * @return The local host names and ip addresses.
+     */
     public static List<String> getLocalIps() {
         List<String> localIps = new ArrayList<>();
         localIps.add("localhost");
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 9c44684..f2d9c57 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -30,6 +30,9 @@
          - "remove" - Removed
     -->
     <release version="2.13.2" date="2020-04-23" description="GA Release 2.13.2">
+      <action issue="LOG4J2-3201" dev="rgoers" type="fix">
+	Limit the protocols JNDI can use by default. Limit the servers and classes that can be accessed via LDAP.
+      </action>
       <action issue="LOG4J2-2824" dev="rgoers" type="fix" due-to="CrazyBills">
         Implement requiresLocation in GelfLayout to reflect whether location information is used in the message Pattern.
       </action>
diff --git a/src/site/xdoc/manual/appenders.xml b/src/site/xdoc/manual/appenders.xml
index ab40094..267f54f 100644
--- a/src/site/xdoc/manual/appenders.xml
+++ b/src/site/xdoc/manual/appenders.xml
@@ -1542,6 +1542,33 @@ public class ConnectionFactory {
               <th>Default</th>
               <th>Description</th>
             </tr>
+            <tr>
+              <td>allowdLdapClasses</td>
+              <td>String</td>
+              <td>null</td>
+              <td>
+                A comma separated list of fully qualified class names that may be accessed by LDAP. The classes
+                must implement Serializable. Only applies when the JMS Appender By default only Java primative classes are allowed.
+              </td>
+            </tr>
+            <tr>
+              <td>allowdLdapHosts</td>
+              <td>String</td>
+              <td>null</td>
+              <td>
+                A comma separated list of host names or ip addresses that may be accessed by LDAP. By default only
+                the local host names and ip addresses are allowed.
+              </td>
+            </tr>
+            <tr>
+              <td>allowdJndiProtocols</td>
+              <td>String</td>
+              <td>null</td>
+              <td>
+                A comma separated list of protocol names that JNDI will allow. By default only java, ldap, and ldaps
+                are the only allowed protocols.
+              </td>
+            </tr>
             <tr>
               <td>factoryBindingName</td>
               <td>String</td>
diff --git a/src/site/xdoc/manual/configuration.xml.vm b/src/site/xdoc/manual/configuration.xml.vm
index a3da3b8..402a96c 100644
--- a/src/site/xdoc/manual/configuration.xml.vm
+++ b/src/site/xdoc/manual/configuration.xml.vm
@@ -1960,6 +1960,32 @@ public class AwesomeTest {
       before falling back to the default class loader.
     </td>
   </tr>
+  <tr>
+    <td><a name="allowedLdapClasses"/>log4j2.allowedLdapClasses</td>
+    <td>LOG4J_ALLOWED_LDAP_CLASSES</td>
+    <td>&nbsp;</td>
+    <td>
+      System property that specifies fully qualified class names that may be accessed by LDAP. The classes
+      must implement Serializable. By default only Java primative classes are allowed.
+    </td>
+  </tr>
+  <tr>
+    <td><a name="allowedLdapHosts"/>log4j2.allowedLdapHosts</td>
+    <td>LOG4J_ALLOWED_LDAP_HOSTS</td>
+    <td>&nbsp;</td>
+    <td>
+      System property that adds host names or ip addresses that may be access by LDAP. By default it only allows
+      the local host names and ip addresses.
+    </td>
+  </tr>
+  <tr>
+    <td><a name="allowedJndiProtocols"/>log4j2.allowedJndiProtocols</td>
+    <td>LOG4J_ALLOWED_JNDI_PROTOCOLS</td>
+    <td>&nbsp;</td>
+    <td>
+      System property that adds protocol names that JNDI will allow. By default it only allows java, ldap, and ldaps.
+    </td>
+  </tr>
   <tr>
     <td><a name="uuidSequence"/>log4j2.uuidSequence
       <br />
diff --git a/src/site/xdoc/manual/extending.xml b/src/site/xdoc/manual/extending.xml
index ba04d68..04c742a 100644
--- a/src/site/xdoc/manual/extending.xml
+++ b/src/site/xdoc/manual/extending.xml
@@ -92,7 +92,10 @@
               <dd>Associates LoggerContexts with the ClassLoader that created the caller of the getLogger call. This is
               the default ContextSelector.</dd>
               <dt><a class="javadoc" href="../log4j-core/apidocs/org/apache/logging/log4j/core/selector/JndiContextSelector.html">JndiContextSelector</a></dt>
-              <dd>Locates the LoggerContext by querying JNDI.</dd>
+              <dd>Locates the LoggerContext by querying JNDI. Please see <a href="../manual/configuration.html#allowedJndiProtocols">log4j2.allowedJndiProtocols</a>,
+                <a href="../manual/configuration.html#allowedLdapClasses">log4j2.allowedLdapClasses</a>, and
+                <a href="../manual/configuration.html#allowedLdapHosts">log4j2.allowedLdapHosts</a> for restrictions on using JNDI
+                with Log4j.</dd>
               <dt><a class="javadoc" href="../log4j-core/apidocs/org/apache/logging/log4j/core/async/AsyncLoggerContextSelector.html">AsyncLoggerContextSelector</a></dt>
               <dd>Creates a LoggerContext that ensures that all loggers are AsyncLoggers.</dd>
               <dt><a class="javadoc" href="../log4j-core/apidocs/org/apache/logging/log4j/core/osgi/BundleContextSelector.html">BundleContextSelector</a></dt>
diff --git a/src/site/xdoc/manual/lookups.xml b/src/site/xdoc/manual/lookups.xml
index d699e78..cc6a66f 100644
--- a/src/site/xdoc/manual/lookups.xml
+++ b/src/site/xdoc/manual/lookups.xml
@@ -270,6 +270,13 @@
             The JndiLookup allows variables to be retrieved via JNDI. By default the key will be prefixed with
             java:comp/env/, however if the key contains a ":" no prefix will be added.
           </p>
+          <p>By default the JDNI Lookup only supports the java, ldap, and ldaps protocols or no protocol. Additional
+            protocols may be supported by specifying them on the <code>log4j2.allowedJndiProtocols</code> property.
+            When using LDAP Java classes that implement the Referenceable interface are not supported for security
+            reasons. Only the Java primative classes are supported by default as well as any classes specified by the
+            <code>log4j2.allowedLdapClasses</code> property. When using LDAP only references to the local host name
+            or ip address are supported along with any hosts or ip addresses listed in the
+            <code>log4j2.allowedLdapHosts</code> property.</p>
           <pre class="prettyprint linenums"><![CDATA[
 <File name="Application" fileName="application.log">
   <PatternLayout>
-- 
2.27.0

