This is an automated email from the ASF dual-hosted git repository.
jbonofre pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/karaf.git
The following commit(s) were added to refs/heads/main by this push:
new c4f16212cd Avoid injection issues in the JDBC datasource (#2654)
c4f16212cd is described below
commit c4f16212cd7de74599a05a8c836669866fe8e106
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Mon May 25 06:34:34 2026 +0100
Avoid injection issues in the JDBC datasource (#2654)
---
jdbc/pom.xml | 5 +
.../karaf/jdbc/internal/JdbcServiceImpl.java | 34 +-
.../karaf/jdbc/internal/JdbcServiceImplTest.java | 421 +++++++++++++++++++++
3 files changed, 456 insertions(+), 4 deletions(-)
diff --git a/jdbc/pom.xml b/jdbc/pom.xml
index 2412f5ab4c..57d6d14ff8 100644
--- a/jdbc/pom.xml
+++ b/jdbc/pom.xml
@@ -72,6 +72,11 @@
<artifactId>org.apache.karaf.shell.core</artifactId>
<optional>true</optional>
</dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git
a/jdbc/src/main/java/org/apache/karaf/jdbc/internal/JdbcServiceImpl.java
b/jdbc/src/main/java/org/apache/karaf/jdbc/internal/JdbcServiceImpl.java
index 54f1a99fce..bc0982b9ce 100644
--- a/jdbc/src/main/java/org/apache/karaf/jdbc/internal/JdbcServiceImpl.java
+++ b/jdbc/src/main/java/org/apache/karaf/jdbc/internal/JdbcServiceImpl.java
@@ -82,10 +82,12 @@ public class JdbcServiceImpl implements JdbcService {
@Override
public void delete(String name) throws Exception {
- String filter =
String.format("(&(service.factoryPid=org.ops4j.datasource)(%s=%s))",
DataSourceFactory.JDBC_DATASOURCE_NAME, name);
+ String filter =
String.format("(&(service.factoryPid=org.ops4j.datasource)(%s=%s))",
DataSourceFactory.JDBC_DATASOURCE_NAME, escapeLdapFilterValue(name));
Configuration[] configs = configAdmin.listConfigurations(filter);
- for (Configuration config : configs) {
- config.delete();
+ if (configs != null) {
+ for (Configuration config : configs) {
+ config.delete();
+ }
}
}
@@ -210,10 +212,11 @@ public class JdbcServiceImpl implements JdbcService {
private ServiceReference<?> lookupDataSource(String name) {
ServiceReference<?>[] references;
try {
+ String escapedName = escapeLdapFilterValue(name);
references = bundleContext.getServiceReferences((String) null,
"(&(|(" + Constants.OBJECTCLASS + "=" +
DataSource.class.getName() + ")"
+ "(" + Constants.OBJECTCLASS + "=" +
XADataSource.class.getName() + "))"
- + "(|(osgi.jndi.service.name=" + name +
")(datasource=" + name + ")(name=" + name + ")(service.id=" + name + ")))");
+ + "(|(osgi.jndi.service.name=" + escapedName +
")(datasource=" + escapedName + ")(name=" + escapedName + ")(service.id=" +
escapedName + ")))");
} catch (InvalidSyntaxException e) {
throw new IllegalArgumentException("Error finding datasource with
name " + name, e);
}
@@ -229,6 +232,29 @@ public class JdbcServiceImpl implements JdbcService {
return references[references.length - 1];
}
+ /**
+ * Escapes special characters in an RFC 1960 / LDAP filter value.
+ * Characters that must be escaped: '\', '*', '(', ')', and NUL (\u0000).
+ */
+ private static String escapeLdapFilterValue(String value) {
+ if (value == null) {
+ return null;
+ }
+ StringBuilder sb = new StringBuilder(value.length() + 8);
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ switch (c) {
+ case '\\': sb.append("\\5c"); break;
+ case '*': sb.append("\\2a"); break;
+ case '(': sb.append("\\28"); break;
+ case ')': sb.append("\\29"); break;
+ case '\0': sb.append("\\00"); break;
+ default: sb.append(c); break;
+ }
+ }
+ return sb.toString();
+ }
+
private int getRank(ServiceReference<?> reference) {
Object rankObj = reference.getProperty(Constants.SERVICE_RANKING);
// If no rank, then spec says it defaults to zero.
diff --git
a/jdbc/src/test/java/org/apache/karaf/jdbc/internal/JdbcServiceImplTest.java
b/jdbc/src/test/java/org/apache/karaf/jdbc/internal/JdbcServiceImplTest.java
new file mode 100644
index 0000000000..95f7555408
--- /dev/null
+++ b/jdbc/src/test/java/org/apache/karaf/jdbc/internal/JdbcServiceImplTest.java
@@ -0,0 +1,421 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.karaf.jdbc.internal;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Filter;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.jdbc.DataSourceFactory;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class JdbcServiceImplTest {
+
+ private static final String[] INJECTION_PAYLOADS = {
+ "*",
+ ")(objectClass=*",
+ "dsA)(service.id>=0",
+ "a*b",
+ "dsA\u0000evil",
+ };
+
+ private JdbcServiceImpl service;
+ private TestConfigurationAdmin configAdmin;
+ private TestBundleContext bundleContext;
+
+ private final List<String> capturedConfigAdminFilters = new ArrayList<>();
+ private final List<String> capturedBundleContextFilters = new
ArrayList<>();
+
+ @Before
+ public void setUp() throws Exception {
+ service = new JdbcServiceImpl();
+ configAdmin = new TestConfigurationAdmin();
+ bundleContext = new TestBundleContext();
+
+ setField(service, "configAdmin", configAdmin);
+ setField(service, "bundleContext", bundleContext);
+ }
+
+ @Test
+ public void datasourcesReturnsValuesInExpectedPriorityOrder() throws
Exception {
+ bundleContext.referencesToReturn = new ServiceReference<?>[] {
+ newServiceReference(singleton("osgi.jndi.service.name", "jndiDs")),
+ newServiceReference(singleton("datasource", "datasourceProp")),
+ newServiceReference(singleton("name", "nameProp")),
+
newServiceReference(singleton(DataSourceFactory.JDBC_DATASOURCE_NAME,
"jdbcNameProp")),
+ newServiceReference(singleton(Constants.SERVICE_ID, 77L))
+ };
+
+ List<String> datasources = service.datasources();
+
+ assertEquals(Arrays.asList("jndiDs", "datasourceProp", "nameProp",
"jdbcNameProp", "77"), datasources);
+ }
+
+ @Test
+ public void datasourceServiceIdsReturnsIdsFromReferences() throws
Exception {
+ bundleContext.referencesToReturn = new ServiceReference<?>[] {
+ newServiceReference(singleton(Constants.SERVICE_ID, 10L)),
+ newServiceReference(singleton(Constants.SERVICE_ID, 20L))
+ };
+
+ List<Long> ids = service.datasourceServiceIds();
+
+ assertEquals(Arrays.asList(10L, 20L), ids);
+ }
+
+ @Test
+ public void createRejectsDuplicateDatasourceName() throws Exception {
+ bundleContext.referencesToReturn = new ServiceReference<?>[] {
+ newServiceReference(singleton("osgi.jndi.service.name",
"alreadyThere"))
+ };
+
+ try {
+ service.create("alreadyThere", "h2", null, "db",
"jdbc:h2:mem:test", "sa", "sa", "DataSource");
+ fail("Expected duplicate datasource rejection");
+ } catch (IllegalArgumentException expected) {
+ assertTrue(expected.getMessage().contains("alreadyThere"));
+ }
+ }
+
+ @Test
+ public void createStoresDatasourceConfigurationProperties() throws
Exception {
+ bundleContext.referencesToReturn = null;
+ Configuration config = newConfigurationProxy();
+ configAdmin.configurationToReturn = config;
+
+ service.create("newDs", "h2", null, "mydb", "jdbc:h2:mem:mydb", "sa",
"secret", "DataSource");
+
+ assertEquals("org.ops4j.datasource", configAdmin.createdFactoryPid);
+ assertNotNull(configAdmin.updatedProperties);
+ assertEquals("newDs",
configAdmin.updatedProperties.get(DataSourceFactory.JDBC_DATASOURCE_NAME));
+ assertEquals("h2",
configAdmin.updatedProperties.get(DataSourceFactory.OSGI_JDBC_DRIVER_NAME));
+ assertEquals("mydb",
configAdmin.updatedProperties.get(DataSourceFactory.JDBC_DATABASE_NAME));
+ assertEquals("jdbc:h2:mem:mydb",
configAdmin.updatedProperties.get(DataSourceFactory.JDBC_URL));
+ assertEquals("sa",
configAdmin.updatedProperties.get(DataSourceFactory.JDBC_USER));
+ assertEquals("secret",
configAdmin.updatedProperties.get(DataSourceFactory.JDBC_PASSWORD));
+ assertEquals("DataSource",
configAdmin.updatedProperties.get("dataSourceType"));
+ }
+
+ @Test
+ public void deleteWildcardMustBeEscaped() throws Exception {
+ capturedConfigAdminFilters.clear();
+ service.delete("*");
+
+ assertEquals(1, capturedConfigAdminFilters.size());
+ String filter = capturedConfigAdminFilters.get(0);
+
+ assertFilterValueIsEscaped("delete(\"*\")", filter, "*", "\\2a");
+ }
+
+ @Test
+ public void deleteCloseParenMustBeEscaped() throws Exception {
+ capturedConfigAdminFilters.clear();
+ service.delete(")(objectClass=*");
+
+ assertEquals(1, capturedConfigAdminFilters.size());
+ String filter = capturedConfigAdminFilters.get(0);
+
+ assertFilterValueIsEscaped("delete(\")\")", filter, ")", "\\29");
+ assertFilterValueIsEscaped("delete(\"(\")", filter, "(", "\\28");
+ assertFilterValueIsEscaped("delete(\"*\")", filter, "*", "\\2a");
+ }
+
+ @Test
+ public void deleteAllPayloadsMustBeEscaped() throws Exception {
+ for (String payload : INJECTION_PAYLOADS) {
+ capturedConfigAdminFilters.clear();
+ service.delete(payload);
+
+ assertFalse("No filter captured for payload: " + payload,
capturedConfigAdminFilters.isEmpty());
+ String filter = capturedConfigAdminFilters.get(0);
+
+ assertNoUnescapedSpecialChars("delete(\"" + payload + "\")",
filter, payload);
+ }
+ }
+
+ @Test
+ public void deleteHandlesNullConfigurationsGracefully() throws Exception {
+ // When no configurations match the filter, listConfigurations returns
null
+ // The delete() method should not throw NPE in this case
+ service.delete("nonexistentDataSource");
+
+ assertEquals(1, capturedConfigAdminFilters.size());
+ // Verify the filter was built with proper escaping
+ String filter = capturedConfigAdminFilters.get(0);
+ assertTrue("Filter should contain escaped name",
filter.contains("nonexistentDataSource"));
+ }
+
+ @Test
+ public void lookupDataSourceWildcardMustBeEscaped() throws Exception {
+ capturedBundleContextFilters.clear();
+ try {
+ service.info("*");
+ } catch (IllegalArgumentException expected) {
+ // expected
+ }
+
+ assertFalse("lookupDataSource filter was never built",
capturedBundleContextFilters.isEmpty());
+ String filter = capturedBundleContextFilters.stream()
+ .filter(f -> f.contains("osgi.jndi.service.name") ||
f.contains("datasource="))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("No lookup filter captured
for info(\"*\")"));
+
+ assertFilterValueIsEscaped("lookupDataSource(\"*\")", filter, "*",
"\\2a");
+ }
+
+ @Test
+ public void lookupDataSourceCloseParenMustBeEscaped() throws Exception {
+ capturedBundleContextFilters.clear();
+ try {
+ service.info(")(objectClass=*");
+ } catch (IllegalArgumentException | InvalidSyntaxException expected) {
+ // expected
+ }
+
+ capturedBundleContextFilters.stream()
+ .filter(f -> f.contains("osgi.jndi.service.name") ||
f.contains("datasource="))
+ .forEach(filter -> {
+ assertFilterValueIsEscaped("lookupDataSource inject)", filter,
")", "\\29");
+ assertFilterValueIsEscaped("lookupDataSource inject(", filter,
"(", "\\28");
+ });
+ }
+
+ @Test
+ public void lookupDataSourceAllPayloadsMustBeEscaped() throws Exception {
+ for (String payload : INJECTION_PAYLOADS) {
+ capturedBundleContextFilters.clear();
+ try {
+ service.info(payload);
+ } catch (Exception expected) {
+ // expected
+ }
+
+ capturedBundleContextFilters.stream()
+ .filter(f -> f.contains("osgi.jndi.service.name") ||
f.contains("datasource="))
+ .forEach(filter ->
+ assertNoUnescapedSpecialChars("lookupDataSource(\"" +
payload + "\")", filter, payload));
+ }
+ }
+
+ private static void assertFilterValueIsEscaped(String context, String
filter, String raw, String escaped) {
+ assertTrue(context + ": escaped form '" + escaped + "' not found in
filter: " + filter,
+ filter.contains(escaped));
+ String[] assignments = filter.split("=");
+ for (int i = 1; i < assignments.length; i++) {
+ String valuePart = assignments[i].split("\\)")[0];
+ if (valuePart.contains(raw) && !valuePart.contains(escaped)) {
+ fail(context + ": unescaped '" + raw + "' found in filter
value: '" + valuePart
+ + "' - full filter: " + filter);
+ }
+ }
+ }
+
+ private static void assertNoUnescapedSpecialChars(String context, String
filter, String payload) {
+ char[] specials = {'*', '(', ')', '\\'};
+ for (char c : specials) {
+ if (payload.indexOf(c) < 0) {
+ continue;
+ }
+ String[] assignments = filter.split("=");
+ for (int i = 1; i < assignments.length; i++) {
+ String valuePart = assignments[i].split("\\)")[0];
+ if (valuePart.contains(String.valueOf(c))) {
+ fail(context + ": unescaped '" + c + "' in filter value '"
+ valuePart
+ + "' - full filter: " + filter);
+ }
+ }
+ }
+ }
+
+ private static void setField(Object target, String fieldName, Object
value) throws Exception {
+ Field f = target.getClass().getDeclaredField(fieldName);
+ f.setAccessible(true);
+ f.set(target, value);
+ }
+
+ private static Dictionary<String, Object> singleton(String key, Object
value) {
+ Dictionary<String, Object> dict = new Hashtable<>();
+ dict.put(key, value);
+ return dict;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static ServiceReference<Object>
newServiceReference(Dictionary<String, Object> properties) {
+ InvocationHandler handler = (proxy, method, args) -> {
+ String methodName = method.getName();
+ if ("getProperty".equals(methodName)) {
+ return properties.get((String) args[0]);
+ }
+ if ("getPropertyKeys".equals(methodName)) {
+ List<String> keys = new ArrayList<>();
+ for (java.util.Enumeration<String> e = properties.keys();
e.hasMoreElements();) {
+ keys.add(e.nextElement());
+ }
+ return keys.toArray(new String[0]);
+ }
+ if ("compareTo".equals(methodName)) {
+ return 0;
+ }
+ if ("toString".equals(methodName)) {
+ return "ServiceReference";
+ }
+ if ("hashCode".equals(methodName)) {
+ return System.identityHashCode(proxy);
+ }
+ if ("equals".equals(methodName)) {
+ return proxy == args[0];
+ }
+ return null;
+ };
+ return (ServiceReference<Object>) Proxy.newProxyInstance(
+ JdbcServiceImplTest.class.getClassLoader(),
+ new Class<?>[] { ServiceReference.class },
+ handler);
+ }
+
+ private Configuration newConfigurationProxy() {
+ InvocationHandler handler = (proxy, method, args) -> {
+ if ("update".equals(method.getName()) && args != null &&
args.length == 1 && args[0] instanceof Dictionary) {
+ @SuppressWarnings("unchecked")
+ Dictionary<String, String> updated = (Dictionary<String,
String>) args[0];
+ configAdmin.updatedProperties = updated;
+ return null;
+ }
+ if ("delete".equals(method.getName())) {
+ configAdmin.deleteCalls++;
+ return null;
+ }
+ if ("toString".equals(method.getName())) {
+ return "Configuration";
+ }
+ return null;
+ };
+ return (Configuration) Proxy.newProxyInstance(
+ JdbcServiceImplTest.class.getClassLoader(),
+ new Class<?>[] { Configuration.class },
+ handler);
+ }
+
+ private class TestConfigurationAdmin implements ConfigurationAdmin {
+ String createdFactoryPid;
+ Configuration configurationToReturn;
+ Dictionary<String, String> updatedProperties;
+ int deleteCalls;
+
+ @Override
+ public Configuration createFactoryConfiguration(String factoryPid) {
+ createdFactoryPid = factoryPid;
+ return configurationToReturn;
+ }
+
+ @Override
+ public Configuration createFactoryConfiguration(String factoryPid,
String location) {
+ createdFactoryPid = factoryPid;
+ return configurationToReturn;
+ }
+
+ @Override
+ public Configuration getConfiguration(String pid, String location) {
+ return null;
+ }
+
+ @Override
+ public Configuration getConfiguration(String pid) {
+ return null;
+ }
+
+ @Override
+ public Configuration getFactoryConfiguration(String factoryPid, String
name, String location) {
+ return null;
+ }
+
+ @Override
+ public Configuration getFactoryConfiguration(String factoryPid, String
name) {
+ return null;
+ }
+
+ @Override
+ public Configuration[] listConfigurations(String filter) {
+ capturedConfigAdminFilters.add(filter);
+ return new Configuration[0];
+ }
+ }
+
+ private class TestBundleContext extends StubBundleContext {
+ ServiceReference<?>[] referencesToReturn;
+
+ @Override
+ public ServiceReference<?>[] getServiceReferences(String clazz, String
filter)
+ throws InvalidSyntaxException {
+ if (filter != null) {
+ capturedBundleContextFilters.add(filter);
+ }
+ return referencesToReturn;
+ }
+ }
+
+ private static abstract class StubBundleContext implements BundleContext {
+
+ @Override public String getProperty(String key) { return null; }
+ @Override public org.osgi.framework.Bundle getBundle() { return null; }
+ @Override public org.osgi.framework.Bundle installBundle(String
location, java.io.InputStream input) { return null; }
+ @Override public org.osgi.framework.Bundle installBundle(String
location) { return null; }
+ @Override public org.osgi.framework.Bundle getBundle(long id) { return
null; }
+ @Override public org.osgi.framework.Bundle[] getBundles() { return new
org.osgi.framework.Bundle[0]; }
+ @Override public void
addServiceListener(org.osgi.framework.ServiceListener listener, String filter)
{ }
+ @Override public void
addServiceListener(org.osgi.framework.ServiceListener listener) { }
+ @Override public void
removeServiceListener(org.osgi.framework.ServiceListener listener) { }
+ @Override public void
addBundleListener(org.osgi.framework.BundleListener listener) { }
+ @Override public void
removeBundleListener(org.osgi.framework.BundleListener listener) { }
+ @Override public void
addFrameworkListener(org.osgi.framework.FrameworkListener listener) { }
+ @Override public void
removeFrameworkListener(org.osgi.framework.FrameworkListener listener) { }
+ @Override public org.osgi.framework.ServiceRegistration<?>
registerService(String[] clazzes, Object service, Dictionary<String, ?>
properties) { return null; }
+ @Override public org.osgi.framework.ServiceRegistration<?>
registerService(String clazz, Object service, Dictionary<String, ?> properties)
{ return null; }
+ @Override public <S> org.osgi.framework.ServiceRegistration<S>
registerService(Class<S> clazz, S service, Dictionary<String, ?> properties) {
return null; }
+ @Override public <S> org.osgi.framework.ServiceRegistration<S>
registerService(Class<S> clazz, org.osgi.framework.ServiceFactory<S> factory,
Dictionary<String, ?> properties) { return null; }
+ @Override public ServiceReference<?>[] getServiceReferences(String
clazz, String filter) throws InvalidSyntaxException { return null; }
+ @Override public ServiceReference<?>[] getAllServiceReferences(String
clazz, String filter) throws InvalidSyntaxException { return null; }
+ @Override public <S> ServiceReference<S> getServiceReference(Class<S>
clazz) { return null; }
+ @Override public <S> java.util.Collection<ServiceReference<S>>
getServiceReferences(Class<S> clazz, String filter) throws
InvalidSyntaxException { return new ArrayList<>(); }
+ @Override public ServiceReference<?> getServiceReference(String clazz)
{ return null; }
+ @Override public <S> S getService(ServiceReference<S> reference) {
return null; }
+ @Override public boolean ungetService(ServiceReference<?> reference) {
return false; }
+ @Override public <S> org.osgi.framework.ServiceObjects<S>
getServiceObjects(ServiceReference<S> reference) { return null; }
+ @Override public java.io.File getDataFile(String filename) { return
null; }
+ @Override public Filter createFilter(String filter) throws
InvalidSyntaxException { return null; }
+ @Override public org.osgi.framework.Bundle getBundle(String location)
{ return null; }
+ }
+}