Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions api/src/main/java/javax/jdo/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

package javax.jdo;

import javax.naming.Context;

/**
* Constant values used in JDO.
*
Expand Down Expand Up @@ -1042,4 +1044,18 @@ public interface Constants {
* @since 2.2
*/
public static final String TX_SERIALIZABLE = "serializable";

/**
* The name of the boolean system property that, when set to "true", allows JNDI locations with a
* URL scheme other than "java" (for example "ldap://..." or "rmi://...") to be passed to the
* JNDI-based {@link JDOHelper#getPersistenceManagerFactory(String, Context, ClassLoader)}
* overloads. Such locations are rejected by default because a URL-scheme lookup selects the
* naming provider from the location string itself and, depending on the JVM and provider
* configuration, can trigger remote class loading or deserialization of untrusted data (JNDI
* injection).
*
* @since 3.3
*/
String PROPERTY_ALLOW_URL_SCHEME_JNDI_LOCATIONS =
"javax.jdo.allowUrlSchemeJndiLocations"; // NOI18N
}
45 changes: 45 additions & 0 deletions api/src/main/java/javax/jdo/JDOHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -748,7 +748,7 @@
continue;
}
// else assume first line of text is the PMF class name
String[] tokens = line.split("\\s");

Check warning on line 751 in api/src/main/java/javax/jdo/JDOHelper.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this regular expression to a Pattern compiled outside the loop.

See more on https://sonarcloud.io/project/issues?id=db-jdo&issues=AaCWXS5X136A9JFWaiPs&open=AaCWXS5X136A9JFWaiPs&pullRequest=136
String pmfClassName = tokens[0];
int indexOfComment = pmfClassName.indexOf("#");
if (indexOfComment == -1) {
Expand Down Expand Up @@ -1452,13 +1452,52 @@
}
}

/**
* Reject JNDI locations carrying a URL scheme other than "java" unless explicitly allowed via the
* system property named by {@link #PROPERTY_ALLOW_URL_SCHEME_JNDI_LOCATIONS}. The JNDI location
* must come from trusted deployment configuration, never from request data.
*
* @param jndiLocation the JNDI location to check
*/
private static void assertPermittedJndiLocation(String jndiLocation) {
int colon = jndiLocation.indexOf(':');
int slash = jndiLocation.indexOf('/');
if (colon <= 0 || (slash != -1 && slash < colon)) {
// no URL scheme: JNDI only treats "scheme:" ahead of any '/' as a URL
return;
}
String scheme = jndiLocation.substring(0, colon);
if (!scheme.matches("[A-Za-z][A-Za-z0-9+.\\-]*")) {
// not a syntactically valid scheme; the provider treats the location as a plain name
return;
}
if ("java".equalsIgnoreCase(scheme)) {
// local component-environment names such as "java:comp/env/jdo/PMF"
return;
}
if (Boolean.getBoolean(PROPERTY_ALLOW_URL_SCHEME_JNDI_LOCATIONS)) {
return;
}
throw new JDOFatalUserException(
MSG.msg(
"EXC_GetPMFUrlSchemeJndiLocation", // NOI18N
jndiLocation,
scheme,
PROPERTY_ALLOW_URL_SCHEME_JNDI_LOCATIONS));
}

/**
* Returns a {@link PersistenceManagerFactory} at the JNDI location specified by <code>
* jndiLocation</code> in the context <code>context</code>. If <code>context</code> is <code>null
* </code>, <code>new InitialContext()</code> will be used. This method is equivalent to invoking
* {@link #getPersistenceManagerFactory(String,Context,ClassLoader)} with <code>
* Thread.currentThread().getContextClassLoader()</code> as the <code>loader</code> argument.
*
* <p><b>Security note:</b> <code>jndiLocation</code> must come from trusted deployment
* configuration, never from request or user data. Locations with a URL scheme other than "java"
* (e.g. "ldap://...") are rejected unless the system property named by {@link
* #PROPERTY_ALLOW_URL_SCHEME_JNDI_LOCATIONS} is set to "true".
*
* @since 2.0
* @param jndiLocation the JNDI location containing the PersistenceManagerFactory
* @param context the context in which to find the named PersistenceManagerFactory
Expand All @@ -1476,6 +1515,11 @@
* PersistenceManagerFactory} with <code>loader</code>. Any <code>NamingException</code>s thrown
* will be wrapped in a {@link JDOFatalUserException}.
*
* <p><b>Security note:</b> <code>jndiLocation</code> must come from trusted deployment
* configuration, never from request or user data. Locations with a URL scheme other than "java"
* (e.g. "ldap://...") are rejected unless the system property named by {@link
* #PROPERTY_ALLOW_URL_SCHEME_JNDI_LOCATIONS} is set to "true".
*
* @since 2.0
* @param jndiLocation the JNDI location containing the PersistenceManagerFactory
* @param context the context in which to find the named PersistenceManagerFactory
Expand All @@ -1487,6 +1531,7 @@
if (jndiLocation == null)
throw new JDOFatalUserException(MSG.msg("EXC_GetPMFNullJndiLoc")); // NOI18N
if (loader == null) throw new JDOFatalUserException(MSG.msg("EXC_GetPMFNullLoader")); // NOI18N
assertPermittedJndiLocation(jndiLocation);
try {
if (context == null) context = new InitialContext();

Expand Down
5 changes: 5 additions & 0 deletions api/src/main/resources/javax/jdo/Bundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ named "{0}" into a java.util.Properties object.
EXC_GetPMFNullJndiLoc: The JNDI location argument to this method cannot be null.
EXC_GetPMFNamingException: A NamingException was thrown while obtaining the \
PersistenceManagerFactory at "{0}" from JNDI.
EXC_GetPMFUrlSchemeJndiLocation: The JNDI location "{0}" uses the URL scheme "{1}". \
URL-scheme JNDI lookups select the naming provider from the location string and can \
trigger remote class loading or deserialization (JNDI injection), so they are \
disabled by default. If this location is trusted deployment configuration, set the \
system property "{2}" to "true" to allow it.
EXC_GetPMFNullPointerException: The PersistenceManagerFactory class must define a static \
method \nPersistenceManagerFactory getPersistenceManagerFactory(Map props). \nThe class "{0}"\n\
defines a non-static getPersistenceManagerFactory(Map props) method.
Expand Down
29 changes: 29 additions & 0 deletions api/src/test/java/javax/jdo/JDOHelperTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,35 @@ void testUnknownStandardProperties() {
}
}

/** Test that a URL-scheme JNDI location is rejected by default. */
@Test
void testGetPMFUrlSchemeJNDIRejected() {
Context context = getInitialContext();
JDOFatalUserException ex =
Assertions.assertThrows(
JDOFatalUserException.class,
() ->
JDOHelper.getPersistenceManagerFactory("ldap://attacker.example:389/cn=x", context),
"URL-scheme JNDI location should result in JDOFatalUserException");
Assertions.assertTrue(
ex.getMessage().contains(JDOHelper.PROPERTY_ALLOW_URL_SCHEME_JNDI_LOCATIONS),
"Exception should mention the opt-in system property but was: " + ex.getMessage());
}

/** Test that a composite-name JNDI location is not blocked by the URL-scheme check. */
@Test
void testGetPMFCompositeNameJNDINotBlockedBySchemeCheck() {
Context context = getInitialContext();
JDOFatalUserException ex =
Assertions.assertThrows(
JDOFatalUserException.class,
() -> JDOHelper.getPersistenceManagerFactory("java:comp/env/jdo/PMF", context),
"Unbound JNDI name should result in JDOFatalUserException");
Assertions.assertFalse(
ex.getMessage().contains(JDOHelper.PROPERTY_ALLOW_URL_SCHEME_JNDI_LOCATIONS),
"Composite name must not be rejected by the URL-scheme check but was: " + ex.getMessage());
}

private Context getInitialContext() {
try {
return new InitialContext();
Expand Down
2 changes: 2 additions & 0 deletions tck/src/main/resources/conf/jdo-signatures.txt
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ public interface javax.jdo.Constants {
= "javax/jdo/jdoquery_3_0.xsd";
static String ANONYMOUS_PERSISTENCE_MANAGER_FACTORY_NAME
= "";
String PROPERTY_ALLOW_URL_SCHEME_JNDI_LOCATIONS
= "javax.jdo.allowUrlSchemeJndiLocations";
public static final String TX_READ_UNCOMMITTED = "read-uncommitted";
public static final String TX_READ_COMMITTED = "read-committed";
public static final String TX_REPEATABLE_READ = "repeatable-read";
Expand Down
Loading