diff --git a/api/src/main/java/javax/jdo/Constants.java b/api/src/main/java/javax/jdo/Constants.java index aed3c4079..411bdc558 100644 --- a/api/src/main/java/javax/jdo/Constants.java +++ b/api/src/main/java/javax/jdo/Constants.java @@ -17,6 +17,8 @@ package javax.jdo; +import javax.naming.Context; + /** * Constant values used in JDO. * @@ -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 } diff --git a/api/src/main/java/javax/jdo/JDOHelper.java b/api/src/main/java/javax/jdo/JDOHelper.java index aa1d0601f..4c590966d 100644 --- a/api/src/main/java/javax/jdo/JDOHelper.java +++ b/api/src/main/java/javax/jdo/JDOHelper.java @@ -1452,6 +1452,40 @@ public static PersistenceManagerFactory getPersistenceManagerFactory( } } + /** + * 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 * jndiLocation in the context context. If context is null @@ -1459,6 +1493,11 @@ public static PersistenceManagerFactory getPersistenceManagerFactory( * {@link #getPersistenceManagerFactory(String,Context,ClassLoader)} with * Thread.currentThread().getContextClassLoader() as the loader argument. * + *

Security note: jndiLocation 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 @@ -1476,6 +1515,11 @@ public static PersistenceManagerFactory getPersistenceManagerFactory( * PersistenceManagerFactory} with loader. Any NamingExceptions thrown * will be wrapped in a {@link JDOFatalUserException}. * + *

Security note: jndiLocation 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 @@ -1487,6 +1531,7 @@ public static PersistenceManagerFactory getPersistenceManagerFactory( 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(); diff --git a/api/src/main/resources/javax/jdo/Bundle.properties b/api/src/main/resources/javax/jdo/Bundle.properties index 208ef4561..58b699ef8 100644 --- a/api/src/main/resources/javax/jdo/Bundle.properties +++ b/api/src/main/resources/javax/jdo/Bundle.properties @@ -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. diff --git a/api/src/test/java/javax/jdo/JDOHelperTest.java b/api/src/test/java/javax/jdo/JDOHelperTest.java index 4da79f1ae..6ee12ec9b 100644 --- a/api/src/test/java/javax/jdo/JDOHelperTest.java +++ b/api/src/test/java/javax/jdo/JDOHelperTest.java @@ -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(); diff --git a/tck/src/main/resources/conf/jdo-signatures.txt b/tck/src/main/resources/conf/jdo-signatures.txt index 01cc8ad77..b7c251a9b 100644 --- a/tck/src/main/resources/conf/jdo-signatures.txt +++ b/tck/src/main/resources/conf/jdo-signatures.txt @@ -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";