-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #32 from pixee/add_jndi_api
Add JNDI helper API
- Loading branch information
Showing
8 changed files
with
176 additions
and
6 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package io.github.pixee.security; | ||
|
||
import javax.naming.Context; | ||
import javax.naming.NamingException; | ||
import java.util.Objects; | ||
import java.util.Set; | ||
import java.util.stream.Collectors; | ||
|
||
/** Offers utilities to defend against JNDI attacks by controlling allowed resources. */ | ||
public final class JNDI { | ||
|
||
private JNDI() {} | ||
|
||
/** | ||
* Looks up a resource in the context, only allowing resources non-URL-based resources and "java:" resources. | ||
*/ | ||
public static LimitedContext limitedContext(final Context context) { | ||
return new ProtocolLimitedContext(context, J8ApiBridge.setOf(UrlProtocol.JAVA)); | ||
} | ||
|
||
/** | ||
* Looks up a resource in the context, only allowing resources from the specified protocols. | ||
*/ | ||
public static LimitedContext limitedContextByProtocol(final Context context, final Set<UrlProtocol> allowedProtocols) { | ||
return new ProtocolLimitedContext(context, allowedProtocols); | ||
} | ||
|
||
/** | ||
* Looks up a resource in the context, only allowing resources with the given names. | ||
*/ | ||
public static LimitedContext limitedContextByResourceName(final Context context, final Set<String> allowedResourceNames) { | ||
return new NameLimitedContext(context, allowedResourceNames); | ||
} | ||
|
||
/** A lookalike method for {@link Context} that allows sandboxing resolution. */ | ||
public interface LimitedContext { | ||
/** | ||
* Looks up a resource in the context, but only allows resources that are in the allowed set. | ||
* | ||
* @param resource the resource to look up | ||
* @return the object bound to the resource | ||
* @throws NamingException if the resource is not allowed or if the lookup fails as per {@link Context#lookup(String)} | ||
*/ | ||
Object lookup(final String resource) throws NamingException; | ||
} | ||
|
||
/** A context which limits protocols. */ | ||
private static class ProtocolLimitedContext implements LimitedContext { | ||
private final Context context; | ||
private final Set<UrlProtocol> allowedProtocols; | ||
|
||
private ProtocolLimitedContext(final Context context, final Set<UrlProtocol> allowedProtocols) { | ||
this.context = Objects.requireNonNull(context); | ||
this.allowedProtocols = Objects.requireNonNull(allowedProtocols); | ||
} | ||
|
||
@Override | ||
public Object lookup(final String resource) throws NamingException { | ||
Set<String> allowedProtocolPrefixes = allowedProtocols.stream().map(UrlProtocol::getKey).map(p -> p + ":").collect(Collectors.toSet()); | ||
String canonicalResource = resource.toLowerCase().trim(); | ||
if(canonicalResource.contains(":")) { | ||
if (allowedProtocolPrefixes.stream().anyMatch(canonicalResource::startsWith)) { | ||
return context.lookup(resource); | ||
} else { | ||
throw new SecurityException("Unexpected JNDI resource protocol: " + resource); | ||
} | ||
} | ||
return context.lookup(resource); | ||
} | ||
} | ||
|
||
/** A context which only allows pre-defined resource names. */ | ||
private static class NameLimitedContext implements LimitedContext { | ||
private final Context context; | ||
private final Set<String> allowedResourceNames; | ||
|
||
private NameLimitedContext(final Context context, final Set<String> allowedResourceNames) { | ||
this.context = Objects.requireNonNull(context); | ||
this.allowedResourceNames = Objects.requireNonNull(allowedResourceNames); | ||
} | ||
@Override | ||
public Object lookup(final String resource) throws NamingException { | ||
if(allowedResourceNames.contains(resource)) { | ||
return context.lookup(resource); | ||
} | ||
throw new SecurityException("Unexpected JNDI resource name: " + resource); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
package io.github.pixee.security; | ||
|
||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Test; | ||
|
||
import javax.naming.Context; | ||
import javax.naming.NamingException; | ||
|
||
import static org.hamcrest.CoreMatchers.is; | ||
import static org.hamcrest.MatcherAssert.assertThat; | ||
import static org.junit.jupiter.api.Assertions.assertThrows; | ||
import static org.mockito.Mockito.*; | ||
|
||
final class JNDITest { | ||
|
||
private Context context; | ||
private final Object NAMED_OBJECT = new Object(); | ||
private final Object JAVA_OBJECT = new Object(); | ||
private final Object LDAP_OBJECT = new Object(); | ||
private final Object RMI_OBJECT = new Object(); | ||
|
||
@BeforeEach | ||
void setup() throws NamingException { | ||
context = mock(Context.class); | ||
when(context.lookup("simple_name")).thenReturn(NAMED_OBJECT); | ||
when(context.lookup("java:comp/env")).thenReturn(JAVA_OBJECT); | ||
when(context.lookup("ldap://localhost:1389/ou=system")).thenReturn(LDAP_OBJECT); | ||
when(context.lookup("rmi://localhost:1099/evil")).thenReturn(RMI_OBJECT); | ||
} | ||
|
||
@Test | ||
void it_limits_resources_by_name() throws NamingException { | ||
JNDI.LimitedContext limitedContext = JNDI.limitedContextByResourceName(context, J8ApiBridge.setOf("simple_name")); | ||
assertThat(limitedContext.lookup("simple_name"), is(NAMED_OBJECT)); | ||
assertThrows(SecurityException.class, () -> limitedContext.lookup("anything_else")); | ||
verify(context, times(1)).lookup(anyString()); | ||
} | ||
|
||
@Test | ||
void it_limits_resources_by_protocol() throws NamingException { | ||
JNDI.LimitedContext onlyJavaContext = JNDI.limitedContextByProtocol(context, J8ApiBridge.setOf(UrlProtocol.JAVA)); | ||
assertThat(onlyJavaContext.lookup("java:comp/env"), is(JAVA_OBJECT)); | ||
|
||
// confirm protocols protections dont restrict simple name lookups | ||
assertThat(onlyJavaContext.lookup("simple_name"), is(NAMED_OBJECT)); | ||
assertThrows(SecurityException.class, () -> onlyJavaContext.lookup("ldap://localhost:1389/ou=system")); | ||
assertThrows(SecurityException.class, () -> onlyJavaContext.lookup("rmi://localhost:1099/evil")); | ||
|
||
JNDI.LimitedContext onlyLdapContext = JNDI.limitedContextByProtocol(context, J8ApiBridge.setOf(UrlProtocol.LDAP)); | ||
assertThat(onlyLdapContext.lookup("ldap://localhost:1389/ou=system"), is(LDAP_OBJECT)); | ||
assertThrows(SecurityException.class, () -> onlyLdapContext.lookup("java:comp/env")); | ||
assertThrows(SecurityException.class, () -> onlyLdapContext.lookup("rmi://localhost:1099/evil")); | ||
|
||
JNDI.LimitedContext onlyLdapAndJavaContext = JNDI.limitedContextByProtocol(context, J8ApiBridge.setOf(UrlProtocol.JAVA, UrlProtocol.LDAP)); | ||
assertThat(onlyLdapAndJavaContext.lookup("ldap://localhost:1389/ou=system"), is(LDAP_OBJECT)); | ||
assertThat(onlyLdapAndJavaContext.lookup("java:comp/env"), is(JAVA_OBJECT)); | ||
assertThrows(SecurityException.class, () -> onlyLdapAndJavaContext.lookup("rmi://localhost:1099/evil")); | ||
} | ||
|
||
@Test | ||
void default_limits_rmi_and_ldap() throws NamingException { | ||
JNDI.LimitedContext defaultLimitedContext = JNDI.limitedContext(context); | ||
assertThat(defaultLimitedContext.lookup("java:comp/env"), is(JAVA_OBJECT)); | ||
|
||
// confirm simple name lookups still work | ||
assertThat(defaultLimitedContext.lookup("simple_name"), is(NAMED_OBJECT)); | ||
assertThrows(SecurityException.class, () -> defaultLimitedContext.lookup("rmi://localhost:1099/evil")); | ||
assertThrows(SecurityException.class, () -> defaultLimitedContext.lookup("ldap://localhost:1389/ou=system")); | ||
} | ||
|
||
} |