Skip to content

Commit d82b47c

Browse files
committed
LOG4J2-3201 - Limit the protocols JNDI can use by default. Limit the servers and classes that can be accessed via LDAP.
1 parent d35b606 commit d82b47c

File tree

12 files changed

+419
-9
lines changed

12 files changed

+419
-9
lines changed

log4j-core/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,11 @@
311311
</exclusion>
312312
</exclusions>
313313
</dependency>
314+
<dependency>
315+
<groupId>org.zapodot</groupId>
316+
<artifactId>embedded-ldap-junit</artifactId>
317+
<scope>test</scope>
318+
</dependency>
314319
</dependencies>
315320
<build>
316321
<plugins>

log4j-core/src/main/java/org/apache/logging/log4j/core/net/JndiManager.java

+114-6
Original file line numberDiff line numberDiff line change
@@ -17,31 +17,69 @@
1717

1818
package org.apache.logging.log4j.core.net;
1919

20+
import java.net.URI;
21+
import java.net.URISyntaxException;
22+
import java.util.ArrayList;
23+
import java.util.Arrays;
24+
import java.util.HashMap;
25+
import java.util.List;
26+
import java.util.Locale;
27+
import java.util.Map;
2028
import java.util.Properties;
2129
import java.util.concurrent.TimeUnit;
2230

2331
import javax.naming.Context;
24-
import javax.naming.InitialContext;
32+
import javax.naming.NamingEnumeration;
2533
import javax.naming.NamingException;
34+
import javax.naming.directory.Attribute;
35+
import javax.naming.directory.Attributes;
36+
import javax.naming.directory.DirContext;
37+
import javax.naming.directory.InitialDirContext;
2638

2739
import org.apache.logging.log4j.core.appender.AbstractManager;
2840
import org.apache.logging.log4j.core.appender.ManagerFactory;
2941
import org.apache.logging.log4j.core.util.JndiCloser;
42+
import org.apache.logging.log4j.core.util.NetUtils;
43+
import org.apache.logging.log4j.util.PropertiesUtil;
3044

3145
/**
32-
* Manages a JNDI {@link javax.naming.Context}.
46+
* Manages a JNDI {@link javax.naming.directory.DirContext}.
3347
*
3448
* @since 2.1
3549
*/
3650
public class JndiManager extends AbstractManager {
3751

52+
public static final String ALLOWED_HOSTS = "allowedLdapHosts";
53+
public static final String ALLOWED_CLASSES = "allowedLdapClasses";
54+
public static final String ALLOWED_PROTOCOLS = "allowedJndiProtocols";
55+
3856
private static final JndiManagerFactory FACTORY = new JndiManagerFactory();
57+
private static final String PREFIX = "log4j2.";
58+
private static final String LDAP = "ldap";
59+
private static final String LDAPS = "ldaps";
60+
private static final String JAVA = "java";
61+
private static final List<String> permanentAllowedHosts = NetUtils.getLocalIps();
62+
private static final List<String> permanentAllowedClasses = Arrays.asList(Boolean.class.getName(),
63+
Byte.class.getName(), Character.class.getName(), Double.class.getName(), Float.class.getName(),
64+
Integer.class.getName(), Long.class.getName(), Short.class.getName(), String.class.getName());
65+
private static final List<String> permanentAllowedProtocols = Arrays.asList(JAVA, LDAP, LDAPS);
66+
private static final String SERIALIZED_DATA = "javaSerializedData";
67+
private static final String CLASS_NAME = "javaClassName";
68+
private static final String REFERENCE_ADDRESS = "javaReferenceAddress";
69+
private static final String OBJECT_FACTORY = "javaFactory";
70+
private final List<String> allowedHosts;
71+
private final List<String> allowedClasses;
72+
private final List<String> allowedProtocols;
3973

40-
private final Context context;
74+
private final DirContext context;
4175

42-
private JndiManager(final String name, final Context context) {
76+
private JndiManager(final String name, final DirContext context, final List<String> allowedHosts,
77+
final List<String> allowedClasses, final List<String> allowedProtocols) {
4378
super(null, name);
4479
this.context = context;
80+
this.allowedHosts = allowedHosts;
81+
this.allowedClasses = allowedClasses;
82+
this.allowedProtocols = allowedProtocols;
4583
}
4684

4785
/**
@@ -168,21 +206,91 @@ protected boolean releaseSub(final long timeout, final TimeUnit timeUnit) {
168206
* @throws NamingException if a naming exception is encountered
169207
*/
170208
@SuppressWarnings("unchecked")
171-
public <T> T lookup(final String name) throws NamingException {
209+
public synchronized <T> T lookup(final String name) throws NamingException {
210+
try {
211+
URI uri = new URI(name);
212+
if (uri.getScheme() != null) {
213+
if (!allowedProtocols.contains(uri.getScheme().toLowerCase(Locale.ROOT))) {
214+
LOGGER.warn("Log4j JNDI does not allow protocol {}", uri.getScheme());
215+
return null;
216+
}
217+
if (LDAP.equalsIgnoreCase(uri.getScheme()) || LDAPS.equalsIgnoreCase(uri.getScheme())) {
218+
if (!allowedHosts.contains(uri.getHost())) {
219+
LOGGER.warn("Attempt to access ldap server not in allowed list");
220+
return null;
221+
}
222+
Attributes attributes = this.context.getAttributes(name);
223+
if (attributes != null) {
224+
// In testing the "key" for attributes seems to be lowercase while the attribute id is
225+
// camelcase, but that may just be true for the test LDAP used here. This copies the Attributes
226+
// to a Map ignoring the "key" and using the Attribute's id as the key in the Map so it matches
227+
// the Java schema.
228+
Map<String, Attribute> attributeMap = new HashMap<>();
229+
NamingEnumeration<? extends Attribute> enumeration = attributes.getAll();
230+
while (enumeration.hasMore()) {
231+
Attribute attribute = enumeration.next();
232+
attributeMap.put(attribute.getID(), attribute);
233+
}
234+
Attribute classNameAttr = attributeMap.get(CLASS_NAME);
235+
if (attributeMap.get(SERIALIZED_DATA) != null) {
236+
if (classNameAttr != null) {
237+
String className = classNameAttr.get().toString();
238+
if (!allowedClasses.contains(className)) {
239+
LOGGER.warn("Deserialization of {} is not allowed", className);
240+
return null;
241+
}
242+
} else {
243+
LOGGER.warn("No class name provided for {}", name);
244+
return null;
245+
}
246+
} else if (attributeMap.get(REFERENCE_ADDRESS) != null
247+
|| attributeMap.get(OBJECT_FACTORY) != null) {
248+
LOGGER.warn("Referenceable class is not allowed for {}", name);
249+
return null;
250+
}
251+
}
252+
}
253+
}
254+
} catch (URISyntaxException ex) {
255+
// This is OK.
256+
}
172257
return (T) this.context.lookup(name);
173258
}
174259

175260
private static class JndiManagerFactory implements ManagerFactory<JndiManager, Properties> {
176261

177262
@Override
178263
public JndiManager createManager(final String name, final Properties data) {
264+
String hosts = data != null ? data.getProperty(ALLOWED_HOSTS) : null;
265+
String classes = data != null ? data.getProperty(ALLOWED_CLASSES) : null;
266+
String protocols = data != null ? data.getProperty(ALLOWED_PROTOCOLS) : null;
267+
List<String> allowedHosts = new ArrayList<>();
268+
List<String> allowedClasses = new ArrayList<>();
269+
List<String> allowedProtocols = new ArrayList<>();
270+
addAll(hosts, allowedHosts, permanentAllowedHosts, ALLOWED_HOSTS, data);
271+
addAll(classes, allowedClasses, permanentAllowedClasses, ALLOWED_CLASSES, data);
272+
addAll(protocols, allowedProtocols, permanentAllowedProtocols, ALLOWED_PROTOCOLS, data);
179273
try {
180-
return new JndiManager(name, new InitialContext(data));
274+
return new JndiManager(name, new InitialDirContext(data), allowedHosts, allowedClasses,
275+
allowedProtocols);
181276
} catch (final NamingException e) {
182277
LOGGER.error("Error creating JNDI InitialContext.", e);
183278
return null;
184279
}
185280
}
281+
282+
private void addAll(String toSplit, List<String> list, List<String> permanentList, String propertyName,
283+
Properties data) {
284+
if (toSplit != null) {
285+
list.addAll(Arrays.asList(toSplit.split("\\s*,\\s*")));
286+
data.remove(propertyName);
287+
}
288+
toSplit = PropertiesUtil.getProperties().getStringProperty(PREFIX + propertyName);
289+
if (toSplit != null) {
290+
list.addAll(Arrays.asList(toSplit.split("\\s*,\\s*")));
291+
}
292+
list.addAll(permanentList);
293+
}
186294
}
187295

188296
@Override

log4j-core/src/main/java/org/apache/logging/log4j/core/util/NetUtils.java

+47
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.apache.logging.log4j.core.util;
1818

1919
import java.io.File;
20+
import java.net.Inet4Address;
2021
import java.net.InetAddress;
2122
import java.net.MalformedURLException;
2223
import java.net.NetworkInterface;
@@ -25,11 +26,14 @@
2526
import java.net.URISyntaxException;
2627
import java.net.URL;
2728
import java.net.UnknownHostException;
29+
import java.util.ArrayList;
2830
import java.util.Arrays;
2931
import java.util.Enumeration;
32+
import java.util.List;
3033

3134
import org.apache.logging.log4j.Logger;
3235
import org.apache.logging.log4j.status.StatusLogger;
36+
import org.apache.logging.log4j.util.Strings;
3337

3438
/**
3539
* Networking-related convenience methods.
@@ -80,6 +84,49 @@ public static String getLocalHostname() {
8084
}
8185
}
8286

87+
/**
88+
* Returns all the local host names and ip addresses.
89+
* @return The local host names and ip addresses.
90+
*/
91+
public static List<String> getLocalIps() {
92+
List<String> localIps = new ArrayList<>();
93+
localIps.add("localhost");
94+
localIps.add("127.0.0.1");
95+
try {
96+
final InetAddress addr = Inet4Address.getLocalHost();
97+
setHostName(addr, localIps);
98+
} catch (final UnknownHostException ex) {
99+
// Ignore this.
100+
}
101+
try {
102+
final Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
103+
if (interfaces != null) {
104+
while (interfaces.hasMoreElements()) {
105+
final NetworkInterface nic = interfaces.nextElement();
106+
final Enumeration<InetAddress> addresses = nic.getInetAddresses();
107+
while (addresses.hasMoreElements()) {
108+
final InetAddress address = addresses.nextElement();
109+
setHostName(address, localIps);
110+
}
111+
}
112+
}
113+
} catch (final SocketException se) {
114+
// ignore.
115+
}
116+
return localIps;
117+
}
118+
119+
private static void setHostName(InetAddress address, List<String> localIps) {
120+
String[] parts = address.toString().split("\\s*/\\s*");
121+
if (parts.length > 0) {
122+
for (String part : parts) {
123+
if (Strings.isNotBlank(part) && !localIps.contains(part)) {
124+
localIps.add(part);
125+
}
126+
}
127+
}
128+
}
129+
83130
/**
84131
* Returns the local network interface's MAC address if possible. The local network interface is defined here as
85132
* the {@link java.net.NetworkInterface} that is both up and not a loopback interface.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache license, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the license for the specific language governing permissions and
15+
* limitations under the license.
16+
*/
17+
package org.apache.logging.log4j.core.lookup;
18+
19+
import java.util.Hashtable;
20+
import javax.naming.Context;
21+
import javax.naming.Name;
22+
import javax.naming.spi.ObjectFactory;
23+
24+
import static org.junit.jupiter.api.Assertions.fail;
25+
26+
/**
27+
* Test LDAP object
28+
*/
29+
public class JndiExploit implements ObjectFactory {
30+
@Override
31+
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment)
32+
throws Exception {
33+
fail("getObjectInstance must not be allowed");
34+
return null;
35+
}
36+
}

0 commit comments

Comments
 (0)