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/camel-karaf.git
The following commit(s) were added to refs/heads/main by this push:
new cc7dbb9cc fix(#729): escape names embedded in OSGi service filters
(#738)
cc7dbb9cc is described below
commit cc7dbb9cc8b777a909d1907179e65f89d00ef40f
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Aug 27 17:53:04 2026 +0200
fix(#729): escape names embedded in OSGi service filters (#738)
Six lookups in camel-core-osgi built a service filter by concatenating a
name straight into it: OsgiBeanRepository (name= and service.pid=),
OsgiComponentResolver (component=), OsgiLanguageResolver (language= and
resolver=) and OsgiDataFormatResolver (dataformat=).
Filter metacharacters in the name were therefore read as filter syntax
rather than matched as text. A name of "*" becomes a presence assertion
matching every registered service, and since each site takes refs[0] the
lookup returned an arbitrary service instead of not resolving. A name
containing ")(" made the framework reject the filter, faulting the
exchange with a syntax error instead of a clean not-found.
Route this through OsgiFilterHelper.createFilter, which escapes the value
first. Escaping only: the FQCN and service.pid interpretations in
lookupByName are the documented purpose of the class and are left alone,
since narrowing them would change behaviour for existing deployments.
getServiceReference(name) at OsgiBeanRepository:80 takes an exact
interface name rather than a filter, so it needs nothing.
Note the escaping is the OSGi filter grammar's backslash-before-character
form, not the RFC 4515 "\2a" hex form - an OSGi Filter reads the latter as
the two literal characters 2a. The tests caught this: they assert against
a real FrameworkUtil.createFilter, so they check the filter actually stops
matching rather than just that a string was rewritten.
---
core/camel-core-osgi/pom.xml | 6 ++
.../camel/karaf/core/OsgiBeanRepository.java | 5 +-
.../camel/karaf/core/OsgiComponentResolver.java | 3 +-
.../camel/karaf/core/OsgiDataFormatResolver.java | 3 +-
.../camel/karaf/core/OsgiLanguageResolver.java | 5 +-
.../camel/karaf/core/utils/OsgiFilterHelper.java | 87 ++++++++++++++++++
.../karaf/core/utils/OsgiFilterHelperTest.java | 102 +++++++++++++++++++++
7 files changed, 205 insertions(+), 6 deletions(-)
diff --git a/core/camel-core-osgi/pom.xml b/core/camel-core-osgi/pom.xml
index 0863bb710..616abaf24 100644
--- a/core/camel-core-osgi/pom.xml
+++ b/core/camel-core-osgi/pom.xml
@@ -73,6 +73,12 @@
<version>${junit-jupiter-version}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-params</artifactId>
+ <version>${junit-jupiter-version}</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
diff --git
a/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiBeanRepository.java
b/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiBeanRepository.java
index 99efc154a..31fcc4ec9 100644
---
a/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiBeanRepository.java
+++
b/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiBeanRepository.java
@@ -27,6 +27,7 @@ import org.apache.camel.RuntimeCamelException;
import org.apache.camel.Service;
import org.apache.camel.spi.BeanRepository;
import org.apache.camel.support.LifecycleStrategySupport;
+import org.apache.camel.karaf.core.utils.OsgiFilterHelper;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
@@ -57,7 +58,7 @@ public class OsgiBeanRepository extends
LifecycleStrategySupport implements Bean
Object service = null;
ServiceReference<?> sr;
try {
- ServiceReference<?>[] refs =
bundleContext.getServiceReferences(type.getName(), "(name=" + name + ")");
+ ServiceReference<?>[] refs =
bundleContext.getServiceReferences(type.getName(),
OsgiFilterHelper.createFilter("name", name));
if (refs != null && refs.length > 0) {
// just return the first one
sr = refs[0];
@@ -80,7 +81,7 @@ public class OsgiBeanRepository extends
LifecycleStrategySupport implements Bean
ServiceReference<?> sr = bundleContext.getServiceReference(name);
if (sr == null) {
// trying to lookup service by PID if not found by name
- String filterExpression = "(" + Constants.SERVICE_PID + "=" + name
+ ")";
+ String filterExpression =
OsgiFilterHelper.createFilter(Constants.SERVICE_PID, name);
try {
ServiceReference<?>[] refs =
bundleContext.getServiceReferences((String)null, filterExpression);
if (refs != null && refs.length > 0) {
diff --git
a/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiComponentResolver.java
b/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiComponentResolver.java
index a80fbdab7..0e2741b35 100644
---
a/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiComponentResolver.java
+++
b/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiComponentResolver.java
@@ -21,6 +21,7 @@ import org.apache.camel.Component;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.spi.ComponentResolver;
import org.apache.camel.support.ResolverHelper;
+import org.apache.camel.karaf.core.utils.OsgiFilterHelper;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
@@ -50,7 +51,7 @@ public class OsgiComponentResolver implements
ComponentResolver {
protected Component getComponent(String name, CamelContext context) throws
Exception {
LOG.trace("Finding Component: {}", name);
try {
- ServiceReference<?>[] refs =
bundleContext.getServiceReferences(ComponentResolver.class.getName(),
"(component=" + name + ")");
+ ServiceReference<?>[] refs =
bundleContext.getServiceReferences(ComponentResolver.class.getName(),
OsgiFilterHelper.createFilter("component", name));
if (refs != null) {
for (ServiceReference<?> ref : refs) {
Object service = bundleContext.getService(ref);
diff --git
a/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiDataFormatResolver.java
b/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiDataFormatResolver.java
index 29d5689e6..c0c414e4f 100644
---
a/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiDataFormatResolver.java
+++
b/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiDataFormatResolver.java
@@ -24,6 +24,7 @@ import org.apache.camel.spi.DataFormat;
import org.apache.camel.spi.DataFormatFactory;
import org.apache.camel.spi.DataFormatResolver;
import org.apache.camel.support.ResolverHelper;
+import org.apache.camel.karaf.core.utils.OsgiFilterHelper;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
@@ -59,7 +60,7 @@ public class OsgiDataFormatResolver implements
DataFormatResolver {
private DataFormat getDataFormat(String name, CamelContext context) {
LOG.trace("Finding DataFormat: {}", name);
try {
- Collection<ServiceReference<DataFormatResolver>> refs =
bundleContext.getServiceReferences(DataFormatResolver.class, "(dataformat=" +
name + ")");
+ Collection<ServiceReference<DataFormatResolver>> refs =
bundleContext.getServiceReferences(DataFormatResolver.class,
OsgiFilterHelper.createFilter("dataformat", name));
if (refs != null) {
for (ServiceReference<DataFormatResolver> ref : refs) {
return
bundleContext.getService(ref).createDataFormat(name, context);
diff --git
a/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiLanguageResolver.java
b/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiLanguageResolver.java
index fa2736e2a..36a7b8897 100644
---
a/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiLanguageResolver.java
+++
b/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiLanguageResolver.java
@@ -22,6 +22,7 @@ import org.apache.camel.RuntimeCamelException;
import org.apache.camel.spi.Language;
import org.apache.camel.spi.LanguageResolver;
import org.apache.camel.support.ResolverHelper;
+import org.apache.camel.karaf.core.utils.OsgiFilterHelper;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
@@ -59,7 +60,7 @@ public class OsgiLanguageResolver implements LanguageResolver
{
protected Language getLanguage(String name, CamelContext context) {
LOG.trace("Finding Language: {}", name);
try {
- ServiceReference<?>[] refs =
bundleContext.getServiceReferences(LanguageResolver.class.getName(),
"(language=" + name + ")");
+ ServiceReference<?>[] refs =
bundleContext.getServiceReferences(LanguageResolver.class.getName(),
OsgiFilterHelper.createFilter("language", name));
if (refs != null) {
for (ServiceReference<?> ref : refs) {
Object service = bundleContext.getService(ref);
@@ -79,7 +80,7 @@ public class OsgiLanguageResolver implements LanguageResolver
{
protected LanguageResolver getLanguageResolver(String name, CamelContext
context) {
LOG.trace("Finding LanguageResolver: {}", name);
try {
- ServiceReference<?>[] refs =
bundleContext.getServiceReferences(LanguageResolver.class.getName(),
"(resolver=" + name + ")");
+ ServiceReference<?>[] refs =
bundleContext.getServiceReferences(LanguageResolver.class.getName(),
OsgiFilterHelper.createFilter("resolver", name));
if (refs != null) {
for (ServiceReference<?> ref : refs) {
Object service = bundleContext.getService(ref);
diff --git
a/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/utils/OsgiFilterHelper.java
b/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/utils/OsgiFilterHelper.java
new file mode 100644
index 000000000..eeb6d7ce3
--- /dev/null
+++
b/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/utils/OsgiFilterHelper.java
@@ -0,0 +1,87 @@
+/*
+ * 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.camel.karaf.core.utils;
+
+/**
+ * Helper to build OSGi service filters from values that are not known to be
filter safe.
+ */
+public final class OsgiFilterHelper {
+
+ private OsgiFilterHelper() {
+ }
+
+ /**
+ * Builds the filter <tt>(key=value)</tt>, escaping the value so it is
matched literally.
+ *
+ * @param key the attribute to match on, must be a literal known to the
caller
+ * @param value the value to match, escaped before being embedded
+ * @return the filter expression
+ */
+ public static String createFilter(String key, String value) {
+ return "(" + key + "=" + escapeFilterValue(value) + ")";
+ }
+
+ /**
+ * Escapes the characters that are significant in an OSGi filter value.
+ * <p/>
+ * The OSGi core specification defines its own filter grammar, in which a
value escapes <tt>(</tt>, <tt>)</tt>,
+ * <tt>*</tt> and <tt>\</tt> by prefixing a single backslash. Note this is
not the <tt>\2a</tt> hex form used by
+ * the LDAP string representation in RFC 4515: an OSGi {@code Filter}
would read that as the two literal
+ * characters <tt>2a</tt>.
+ * <p/>
+ * Without escaping, a name is parsed as filter syntax rather than matched
as text, so <tt>*</tt> becomes a
+ * presence assertion matching every registered service, and an unbalanced
parenthesis makes the framework reject
+ * the filter instead of simply not matching.
+ *
+ * @param value the value to escape, may be <tt>null</tt>
+ * @return the escaped value, or <tt>null</tt> if the given value
was <tt>null</tt>
+ */
+ public static String escapeFilterValue(String value) {
+ if (value == null) {
+ return null;
+ }
+ int first = indexOfSignificantCharacter(value);
+ if (first == -1) {
+ // legitimate component, language, dataformat and bean names never
need escaping
+ return value;
+ }
+ StringBuilder sb = new StringBuilder(value.length() + 8);
+ sb.append(value, 0, first);
+ for (int i = first; i < value.length(); i++) {
+ char ch = value.charAt(i);
+ switch (ch) {
+ case '*', '(', ')', '\\' -> sb.append('\\').append(ch);
+ default -> sb.append(ch);
+ }
+ }
+ return sb.toString();
+ }
+
+ private static int indexOfSignificantCharacter(String value) {
+ for (int i = 0; i < value.length(); i++) {
+ switch (value.charAt(i)) {
+ case '*', '(', ')', '\\' -> {
+ return i;
+ }
+ default -> {
+ // keep looking
+ }
+ }
+ }
+ return -1;
+ }
+}
diff --git
a/core/camel-core-osgi/src/test/java/org/apache/camel/karaf/core/utils/OsgiFilterHelperTest.java
b/core/camel-core-osgi/src/test/java/org/apache/camel/karaf/core/utils/OsgiFilterHelperTest.java
new file mode 100644
index 000000000..5edffaa60
--- /dev/null
+++
b/core/camel-core-osgi/src/test/java/org/apache/camel/karaf/core/utils/OsgiFilterHelperTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.camel.karaf.core.utils;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class OsgiFilterHelperTest {
+
+ @Test
+ public void testNullValue() {
+ assertNull(OsgiFilterHelper.escapeFilterValue(null));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"jms", "http", "aws2-s3", "camel-bean", "xpath",
"org.apache.camel.MyBean", "a+b"})
+ public void testLegitimateNamesAreUnchanged(String name) {
+ assertEquals(name, OsgiFilterHelper.escapeFilterValue(name));
+ assertEquals("(component=" + name + ")",
OsgiFilterHelper.createFilter("component", name));
+ }
+
+ @Test
+ public void testSignificantCharactersAreEscaped() {
+ // the OSGi filter grammar escapes with a backslash before the
character, not with the
+ // RFC 4515 hex form - an OSGi Filter reads "\\2a" as the two literal
characters 2a
+ assertEquals("\\*", OsgiFilterHelper.escapeFilterValue("*"));
+ assertEquals("\\(", OsgiFilterHelper.escapeFilterValue("("));
+ assertEquals("\\)", OsgiFilterHelper.escapeFilterValue(")"));
+ assertEquals("\\\\", OsgiFilterHelper.escapeFilterValue("\\"));
+ assertEquals("a\\*b", OsgiFilterHelper.escapeFilterValue("a*b"));
+ }
+
+ /**
+ * The point of the escaping: a wildcard name must stop selecting every
registered service.
+ */
+ @Test
+ public void testWildcardNoLongerMatchesAnArbitraryService() throws
InvalidSyntaxException {
+ Filter filter =
FrameworkUtil.createFilter(OsgiFilterHelper.createFilter("name", "*"));
+ assertFalse(filter.match(properties("name", "someRegisteredBean")),
+ "an escaped * must not match an unrelated service");
+ assertFalse(filter.match(properties("name", "anotherBean")),
+ "an escaped * must not match an unrelated service");
+ assertTrue(filter.match(properties("name", "*")),
+ "it must still match a service whose name really is *");
+ }
+
+ @Test
+ public void testUnescapedWildcardWouldHaveMatched() throws
InvalidSyntaxException {
+ // documents the behaviour being fixed
+ Filter unescaped = FrameworkUtil.createFilter("(name=*)");
+ assertTrue(unescaped.match(properties("name", "someRegisteredBean")));
+ }
+
+ @Test
+ public void testInjectedFilterSyntaxIsNeutralised() throws
InvalidSyntaxException {
+ String hostile =
"x)(objectClass=org.apache.karaf.features.FeaturesService";
+ Filter filter = assertDoesNotThrow(() ->
FrameworkUtil.createFilter(OsgiFilterHelper.createFilter("name", hostile)),
+ "an escaped name must produce a valid filter rather than a
syntax error");
+ assertFalse(filter.match(properties("name", "someRegisteredBean")));
+ assertFalse(filter.match(properties("objectClass",
"org.apache.karaf.features.FeaturesService")));
+ assertTrue(filter.match(properties("name", hostile)));
+ }
+
+ @Test
+ public void testUnbalancedParenthesisNoLongerFaultsTheFilter() {
+ assertDoesNotThrow(() ->
FrameworkUtil.createFilter(OsgiFilterHelper.createFilter("component", ")(")),
+ "an unbalanced parenthesis must not make the framework reject
the filter");
+ }
+
+ private static Dictionary<String, Object> properties(String key, Object
value) {
+ Dictionary<String, Object> d = new Hashtable<>();
+ d.put(key, value);
+ return d;
+ }
+}