From 977c222ec554522305a27ef82b46317371b1be3f Mon Sep 17 00:00:00 2001 From: Tilmann Date: Sat, 12 Sep 2026 18:03:22 +0200 Subject: [PATCH 1/2] JNDI schema guard --- api/src/main/java/javax/jdo/JDOHelper.java | 58 +++++++++++++++++++ .../resources/javax/jdo/Bundle.properties | 5 ++ .../test/java/javax/jdo/JDOHelperTest.java | 31 ++++++++++ 3 files changed, 94 insertions(+) diff --git a/api/src/main/java/javax/jdo/JDOHelper.java b/api/src/main/java/javax/jdo/JDOHelper.java index aa1d0601f..6268bcd10 100644 --- a/api/src/main/java/javax/jdo/JDOHelper.java +++ b/api/src/main/java/javax/jdo/JDOHelper.java @@ -1452,6 +1452,53 @@ public static PersistenceManagerFactory getPersistenceManagerFactory( } } + /** + * 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 #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 + */ + public static final String PROPERTY_ALLOW_URL_SCHEME_JNDI_LOCATIONS = + "javax.jdo.allowUrlSchemeJndiLocations"; // NOI18N + + /** + * 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 +1506,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 +1528,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 +1544,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..4383f4ecc 100644 --- a/api/src/test/java/javax/jdo/JDOHelperTest.java +++ b/api/src/test/java/javax/jdo/JDOHelperTest.java @@ -500,6 +500,37 @@ 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(); From c0b268dd6a36b34d86e64a5069f03b1a763a36b0 Mon Sep 17 00:00:00 2001 From: Tilmann Date: Sun, 13 Sep 2026 14:36:03 +0200 Subject: [PATCH 2/2] Fix signature file and move property to Constants --- api/src/main/java/javax/jdo/Constants.java | 16 ++++++++++++++++ api/src/main/java/javax/jdo/JDOHelper.java | 19 +++---------------- .../test/java/javax/jdo/JDOHelperTest.java | 6 ++---- .../main/resources/conf/jdo-signatures.txt | 2 ++ 4 files changed, 23 insertions(+), 20 deletions(-) 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 6268bcd10..4c590966d 100644 --- a/api/src/main/java/javax/jdo/JDOHelper.java +++ b/api/src/main/java/javax/jdo/JDOHelper.java @@ -1453,22 +1453,9 @@ public static PersistenceManagerFactory getPersistenceManagerFactory( } /** - * 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 #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 - */ - public static final String PROPERTY_ALLOW_URL_SCHEME_JNDI_LOCATIONS = - "javax.jdo.allowUrlSchemeJndiLocations"; // NOI18N - - /** - * 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. + * 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 */ diff --git a/api/src/test/java/javax/jdo/JDOHelperTest.java b/api/src/test/java/javax/jdo/JDOHelperTest.java index 4383f4ecc..6ee12ec9b 100644 --- a/api/src/test/java/javax/jdo/JDOHelperTest.java +++ b/api/src/test/java/javax/jdo/JDOHelperTest.java @@ -508,8 +508,7 @@ void testGetPMFUrlSchemeJNDIRejected() { Assertions.assertThrows( JDOFatalUserException.class, () -> - JDOHelper.getPersistenceManagerFactory( - "ldap://attacker.example:389/cn=x", context), + 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), @@ -527,8 +526,7 @@ void testGetPMFCompositeNameJNDINotBlockedBySchemeCheck() { "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()); + "Composite name must not be rejected by the URL-scheme check but was: " + ex.getMessage()); } private Context getInitialContext() { 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";