Skip to content

Commit 29d0727

Browse files
committed
add jndi helper tools
1 parent 6d6b294 commit 29d0727

File tree

3 files changed

+160
-1
lines changed

3 files changed

+160
-1
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package io.github.pixee.security;
2+
3+
import javax.naming.Context;
4+
import javax.naming.NamingException;
5+
import java.util.Objects;
6+
import java.util.Set;
7+
import java.util.stream.Collectors;
8+
9+
/** Offers utilities to defend against JNDI attacks by controlling allowed resources. */
10+
public final class JNDI {
11+
12+
private JNDI() {}
13+
14+
/**
15+
* Looks up a resource in the context, only allowing resources non-URL-based resources and "java:" resources.
16+
*/
17+
public static LimitedContext limitedContext(final Context context) {
18+
return new ProtocolLimitedContext(context, J8ApiBridge.setOf(UrlProtocol.JAVA));
19+
}
20+
21+
/**
22+
* Looks up a resource in the context, only allowing resources from the specified protocols.
23+
*/
24+
public static LimitedContext limitedContextByProtocol(final Context context, final Set<UrlProtocol> allowedProtocols) {
25+
return new ProtocolLimitedContext(context, allowedProtocols);
26+
}
27+
28+
/**
29+
* Looks up a resource in the context, only allowing resources with the given names.
30+
*/
31+
public static LimitedContext limitedContextByResourceName(final Context context, final Set<String> allowedResourceNames) {
32+
return new NameLimitedContext(context, allowedResourceNames);
33+
}
34+
35+
/** A lookalike method for {@link Context} that allows sandboxing resolution. */
36+
public interface LimitedContext {
37+
/**
38+
* Looks up a resource in the context, but only allows resources that are in the allowed set.
39+
*
40+
* @param resource the resource to look up
41+
* @return the object bound to the resource
42+
* @throws NamingException if the resource is not allowed or if the lookup fails as per {@link Context#lookup(String)}
43+
*/
44+
Object lookup(final String resource) throws NamingException;
45+
}
46+
47+
/** A context which limits protocols. */
48+
private static class ProtocolLimitedContext implements LimitedContext {
49+
private final Context context;
50+
private final Set<UrlProtocol> allowedProtocols;
51+
52+
private ProtocolLimitedContext(final Context context, final Set<UrlProtocol> allowedProtocols) {
53+
this.context = Objects.requireNonNull(context);
54+
this.allowedProtocols = Objects.requireNonNull(allowedProtocols);
55+
}
56+
57+
@Override
58+
public Object lookup(final String resource) throws NamingException {
59+
Set<String> allowedProtocolPrefixes = allowedProtocols.stream().map(UrlProtocol::getKey).map(p -> p + ":").collect(Collectors.toSet());
60+
String canonicalResource = resource.toLowerCase().trim();
61+
if (allowedProtocolPrefixes.stream().anyMatch(canonicalResource::startsWith)) {
62+
return context.lookup(resource);
63+
}
64+
throw new SecurityException("Unexpected JNDI resource protocol: " + resource);
65+
}
66+
}
67+
68+
/** A context which only allows pre-defined resource names. */
69+
private static class NameLimitedContext implements LimitedContext {
70+
private final Context context;
71+
private final Set<String> allowedResourceNames;
72+
73+
private NameLimitedContext(final Context context, final Set<String> allowedResourceNames) {
74+
this.context = Objects.requireNonNull(context);
75+
this.allowedResourceNames = Objects.requireNonNull(allowedResourceNames);
76+
}
77+
@Override
78+
public Object lookup(final String resource) throws NamingException {
79+
if(allowedResourceNames.contains(resource)) {
80+
return context.lookup(resource);
81+
}
82+
throw new SecurityException("Unexpected JNDI resource name: " + resource);
83+
}
84+
}
85+
}

src/main/java/io/github/pixee/security/UrlProtocol.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,16 @@ public enum UrlProtocol {
4949
TELNET("telnet"),
5050

5151
/** Classpath */
52-
CLASSPATH("classpath");
52+
CLASSPATH("classpath"),
53+
54+
/** LDAP */
55+
LDAP("ldap"),
56+
57+
/** Java */
58+
JAVA("java"),
59+
60+
/** RMI */
61+
RMI("rmi");
5362

5463
private final String key;
5564

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package io.github.pixee.security;
2+
3+
import org.junit.jupiter.api.BeforeEach;
4+
import org.junit.jupiter.api.Test;
5+
6+
import javax.naming.Context;
7+
import javax.naming.NamingException;
8+
9+
import static org.hamcrest.CoreMatchers.is;
10+
import static org.hamcrest.MatcherAssert.assertThat;
11+
import static org.junit.jupiter.api.Assertions.assertThrows;
12+
import static org.mockito.Mockito.*;
13+
14+
final class JNDITest {
15+
16+
private Context context;
17+
private final Object NAMED_OBJECT = new Object();
18+
private final Object JAVA_OBJECT = new Object();
19+
private final Object LDAP_OBJECT = new Object();
20+
private final Object RMI_OBJECT = new Object();
21+
22+
@BeforeEach
23+
void setup() throws NamingException {
24+
context = mock(Context.class);
25+
when(context.lookup("simple_name")).thenReturn(NAMED_OBJECT);
26+
when(context.lookup("java:comp/env")).thenReturn(JAVA_OBJECT);
27+
when(context.lookup("ldap://localhost:1389/ou=system")).thenReturn(LDAP_OBJECT);
28+
when(context.lookup("rmi://localhost:1099/evil")).thenReturn(RMI_OBJECT);
29+
}
30+
31+
@Test
32+
void it_limits_resources_by_name() throws NamingException {
33+
JNDI.LimitedContext limitedContext = JNDI.limitedContextByResourceName(context, J8ApiBridge.setOf("simple_name"));
34+
assertThat(limitedContext.lookup("simple_name"), is(NAMED_OBJECT));
35+
assertThrows(SecurityException.class, () -> limitedContext.lookup("anything_else"));
36+
verify(context, times(1)).lookup(anyString());
37+
}
38+
39+
@Test
40+
void it_limits_resources_by_protocol() throws NamingException {
41+
JNDI.LimitedContext onlyJavaContext = JNDI.limitedContextByProtocol(context, J8ApiBridge.setOf(UrlProtocol.JAVA));
42+
assertThat(onlyJavaContext.lookup("java:comp/env"), is(JAVA_OBJECT));
43+
assertThrows(SecurityException.class, () -> onlyJavaContext.lookup("ldap://localhost:1389/ou=system"));
44+
assertThrows(SecurityException.class, () -> onlyJavaContext.lookup("rmi://localhost:1099/evil"));
45+
46+
JNDI.LimitedContext onlyLdapContext = JNDI.limitedContextByProtocol(context, J8ApiBridge.setOf(UrlProtocol.LDAP));
47+
assertThat(onlyLdapContext.lookup("ldap://localhost:1389/ou=system"), is(LDAP_OBJECT));
48+
assertThrows(SecurityException.class, () -> onlyLdapContext.lookup("java:comp/env"));
49+
assertThrows(SecurityException.class, () -> onlyLdapContext.lookup("rmi://localhost:1099/evil"));
50+
51+
JNDI.LimitedContext onlyLdapAndJavaContext = JNDI.limitedContextByProtocol(context, J8ApiBridge.setOf(UrlProtocol.JAVA, UrlProtocol.LDAP));
52+
assertThat(onlyLdapAndJavaContext.lookup("ldap://localhost:1389/ou=system"), is(LDAP_OBJECT));
53+
assertThat(onlyLdapAndJavaContext.lookup("java:comp/env"), is(JAVA_OBJECT));
54+
assertThrows(SecurityException.class, () -> onlyLdapAndJavaContext.lookup("rmi://localhost:1099/evil"));
55+
}
56+
57+
@Test
58+
void default_limits_rmi_and_ldap() throws NamingException {
59+
JNDI.LimitedContext defaultLimitedContext = JNDI.limitedContext(context);
60+
assertThat(defaultLimitedContext.lookup("java:comp/env"), is(JAVA_OBJECT));
61+
assertThrows(SecurityException.class, () -> defaultLimitedContext.lookup("rmi://localhost:1099/evil"));
62+
assertThrows(SecurityException.class, () -> defaultLimitedContext.lookup("ldap://localhost:1389/ou=system"));
63+
}
64+
65+
}

0 commit comments

Comments
 (0)