This is an automated email from the ASF dual-hosted git repository.

jbonofre pushed a commit to branch karaf-4.4.x
in repository https://gitbox.apache.org/repos/asf/karaf.git


The following commit(s) were added to refs/heads/karaf-4.4.x by this push:
     new 65bab006de fix: port security improvement (XSS) from Felix WebConsole 
(#2729)
65bab006de is described below

commit 65bab006de592468ad6870065f450c0bfae30eae
Author: JB Onofré <[email protected]>
AuthorDate: Sat Sep 12 09:27:49 2026 +0200

    fix: port security improvement (XSS) from Felix WebConsole (#2729)
---
 webconsole/console/pom.xml                         |  11 +-
 .../webconsole/internal/core/ServicesServlet.java  | 469 +++++++++++++++++++++
 2 files changed, 478 insertions(+), 2 deletions(-)

diff --git a/webconsole/console/pom.xml b/webconsole/console/pom.xml
index a90b0ffb4a..11a5fc8351 100644
--- a/webconsole/console/pom.xml
+++ b/webconsole/console/pom.xml
@@ -178,8 +178,15 @@
                             org.osgi.service.wireadmin
                         </DynamicImport-Package>
                         <Embed-Dependency>
-                            <!-- Webconsole -->
-                            org.apache.felix.webconsole;inline=**,
+                            <!--
+                                CVE-2025-25247 (FELIX-6751): the Felix 
WebConsole 4.x line
+                                up to 4.9.8 ships a vulnerable ServicesServlet 
that uses HTML
+                                encoding for a value rendered into a 
JavaScript string literal.
+                                4.9.x ships the upstream fix but requires 
jakarta.servlet, which
+                                Karaf 4.4.x does not provide. Exclude the 
vulnerable class from
+                                the inline so the patched copy in 
src/main/java wins.
+                            -->
+                            
org.apache.felix.webconsole;inline="!org/apache/felix/webconsole/internal/core/ServicesServlet*.class,**",
 
                             <!-- Import/Export-Package parsing -->
                             
org.apache.felix.utils.manifest;inline=org/apache/felix/utils/manifest/**,
diff --git 
a/webconsole/console/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java
 
b/webconsole/console/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java
new file mode 100644
index 0000000000..9fbe27547a
--- /dev/null
+++ 
b/webconsole/console/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java
@@ -0,0 +1,469 @@
+/*
+ * 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.
+ */
+// Original file from Apache Felix WebConsole, see
+// 
https://github.com/apache/felix-dev/blob/org.apache.felix.webconsole-4.8.12/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java
+// with the addition of the escapeJavaScript() method to fix CVE-2025-25247 
(FELIX-6751).
+package org.apache.felix.webconsole.internal.core;
+
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Locale;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.felix.utils.json.JSONWriter;
+import org.apache.felix.webconsole.DefaultVariableResolver;
+import org.apache.felix.webconsole.SimpleWebConsolePlugin;
+import org.apache.felix.webconsole.WebConsoleConstants;
+import org.apache.felix.webconsole.WebConsoleUtil;
+import org.apache.felix.webconsole.internal.OsgiManagerPlugin;
+import org.apache.felix.webconsole.internal.Util;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+
+
+/**
+ * ServicesServlet provides a plugin for inspecting the registered services.
+ */
+public class ServicesServlet extends SimpleWebConsolePlugin implements 
OsgiManagerPlugin
+{
+    // don't create empty reference array all the time, create it only once - 
it is immutable
+    private static final ServiceReference[] NO_REFS = new ServiceReference[0];
+
+    private final class RequestInfo
+    {
+        public final String extension;
+        public final ServiceReference service;
+        public final boolean serviceRequested;
+
+
+        protected RequestInfo( final HttpServletRequest request )
+        {
+            String info = request.getPathInfo();
+            // remove label and starting slash
+            info = info.substring( getLabel().length() + 1 );
+
+            // get extension
+            if ( info.endsWith( ".json" ) )
+            {
+                extension = "json";
+                info = info.substring( 0, info.length() - 5 );
+            }
+            else
+            {
+                extension = "html";
+            }
+
+            // we only accept direct requests to a service if they have a slash
+            // after the label
+            String serviceInfo = null;
+            if ( info.startsWith( "/" ) )
+            {
+                serviceInfo = info.substring( 1 );
+            }
+            if ( serviceInfo == null || serviceInfo.length() == 0 )
+            {
+                service = null;
+                serviceRequested = false;
+            }
+            else
+            {
+                service = getServiceById( serviceInfo );
+                serviceRequested = true;
+            }
+            request.setAttribute( ServicesServlet.class.getName(), this );
+        }
+
+    }
+
+
+    static RequestInfo getRequestInfo( final HttpServletRequest request )
+    {
+        return ( RequestInfo ) request.getAttribute( 
ServicesServlet.class.getName() );
+    }
+
+    /** the label for the services plugin */
+    public static final String LABEL = "services"; //$NON-NLS-1$
+    private static final String TITLE = "%services.pluginTitle"; //$NON-NLS-1$
+    private static final String CSS[] = null;
+
+    // an LDAP filter, that is used to search services
+    private static final String FILTER_PARAM = "filter";
+
+    private final String TEMPLATE;
+
+    /** Default constructor */
+    public ServicesServlet() {
+        super(LABEL, TITLE, CATEGORY_OSGI, CSS);
+
+        // load templates
+        TEMPLATE = readTemplateFile( "/templates/services.html" ); 
//$NON-NLS-1$
+    }
+
+    private ServiceRegistration bipReg;
+
+    public void activate(BundleContext bundleContext)
+    {
+        super.activate(bundleContext);
+        bipReg = new ServicesUsedInfoProvider( bundleContext.getBundle() 
).register( bundleContext );
+    }
+
+    public void deactivate() {
+        if ( null != bipReg )
+        {
+            bipReg.unregister();
+            bipReg = null;
+        }
+        super.deactivate();
+    }
+
+
+    final ServiceReference getServiceById( String pathInfo )
+    {
+        // only use last part of the pathInfo
+        pathInfo = pathInfo.substring( pathInfo.lastIndexOf( '/' ) + 1 );
+
+        StringBuffer filter = new StringBuffer();
+        filter.append( "(" ).append( Constants.SERVICE_ID ).append( "=" );
+        filter.append( pathInfo ).append( ")" );
+        String filterStr = filter.toString();
+        try
+        {
+            ServiceReference[] refs = 
BundleContextUtil.getWorkingBundleContext(this.getBundleContext()).getAllServiceReferences(
 null, filterStr );
+            if ( refs == null || refs.length != 1 )
+            {
+                return null;
+            }
+            return refs[0];
+        }
+        catch ( InvalidSyntaxException e )
+        {
+            log( "Unable to search for services using filter " + filterStr, e 
);
+            // this shouldn't happen
+            return null;
+        }
+    }
+
+
+    private final ServiceReference[] getServices(String filter)
+    {
+        // empty filter string will return nothing, must set it to null to 
return all services
+        if (filter != null && filter.trim().length() == 0) {
+            filter = null;
+        }
+        try
+        {
+            final ServiceReference[] refs = 
BundleContextUtil.getWorkingBundleContext(this.getBundleContext()).getAllServiceReferences(
 null, filter );
+            if ( refs != null )
+            {
+                return refs;
+            }
+        }
+        catch ( InvalidSyntaxException e )
+        {
+            log( "Unable to access service reference list.", e );
+        }
+
+        // no services or invalid filter syntax (unlikely)
+        return NO_REFS;
+    }
+
+
+    static final String getStatusLine( final ServiceReference[] services )
+    {
+        final int count = services.length;
+        final StringBuffer buffer = new StringBuffer();
+        buffer.append( count );
+        buffer.append( " service" );
+        if ( count != 1 )
+            buffer.append( 's' );
+        buffer.append( " in total" );
+        return buffer.toString();
+    }
+
+
+    static final String propertyAsString( ServiceReference ref, String name )
+    {
+        final Object value = ref.getProperty( name );
+        return WebConsoleUtil.toString( value );
+    }
+
+
+    private void renderJSON( final HttpServletResponse response, final 
ServiceReference service, final Locale locale )
+            throws IOException
+    {
+        response.setContentType( "application/json" );
+        response.setCharacterEncoding( "UTF-8" );
+
+        final PrintWriter pw = response.getWriter();
+        writeJSON( pw, service, locale, null);
+    }
+
+    private void keyVal( JSONWriter jw, String key, Object val) throws 
IOException
+    {
+        if ( val != null )
+        {
+            jw.object();
+            jw.key("key").value(key);
+            jw.key("value").value(val);
+            jw.endObject();
+        }
+    }
+
+    private void serviceDetails( JSONWriter jw, ServiceReference service ) 
throws IOException
+    {
+        String[] keys = service.getPropertyKeys();
+
+        jw.key( "props" );
+        jw.array();
+
+        for ( int i = 0; i < keys.length; i++ )
+        {
+            String key = keys[i];
+            if ( Constants.SERVICE_PID.equals( key ) )
+            {
+                keyVal(jw, "Service PID", service.getProperty( key ));
+            }
+            else if ( Constants.SERVICE_DESCRIPTION.equals( key ) )
+            {
+                keyVal(jw, "Service Description", service.getProperty( key ));
+            }
+            else if ( Constants.SERVICE_VENDOR.equals( key ) )
+            {
+                keyVal(jw, "Service Vendor", service.getProperty( key ));
+            }
+            else if ( !Constants.OBJECTCLASS.equals( key ) && 
!Constants.SERVICE_ID.equals( key ) )
+            {
+                keyVal(jw, key, service.getProperty( key ));
+            }
+
+        }
+
+        jw.endArray();
+
+    }
+
+
+    private void usingBundles( JSONWriter jw, ServiceReference service, Locale 
locale ) throws IOException
+    {
+        jw.key( "usingBundles" );
+        jw.array();
+
+        Bundle[] usingBundles = service.getUsingBundles();
+        if ( usingBundles != null )
+        {
+            for ( int i = 0; i < usingBundles.length; i++ )
+            {
+                jw.object();
+                bundleInfo( jw, usingBundles[i], locale );
+                jw.endObject();
+            }
+        }
+
+        jw.endArray();
+
+    }
+
+
+    private void serviceInfo( JSONWriter jw, ServiceReference service, boolean 
details, final Locale locale )
+            throws IOException
+    {
+        jw.object();
+        jw.key( "id" );
+        jw.value( propertyAsString( service, Constants.SERVICE_ID ) );
+        jw.key( "types" );
+        jw.value( propertyAsString( service, Constants.OBJECTCLASS ) );
+        jw.key( "pid" );
+        jw.value( propertyAsString( service, Constants.SERVICE_PID ) );
+        jw.key( "ranking" );
+        final Object ranking = service.getProperty(Constants.SERVICE_RANKING);
+        if ( ranking != null )
+        {
+            jw.value( ranking.toString() );
+        }
+        else
+        {
+            jw.value("");
+        }
+        bundleInfo( jw, service.getBundle(), locale );
+
+        if ( details )
+        {
+            serviceDetails( jw, service );
+            usingBundles( jw, service, locale );
+        }
+
+        jw.endObject();
+    }
+
+
+    private void bundleInfo( final JSONWriter jw, final Bundle bundle, final 
Locale locale )
+            throws IOException
+    {
+        jw.key( "bundleId" );
+        jw.value( bundle.getBundleId() );
+        jw.key( "bundleName" );
+        jw.value( Util.getName( bundle, locale ) );
+        jw.key( "bundleVersion" );
+        jw.value( Util.getHeaderValue( bundle, Constants.BUNDLE_VERSION ) );
+        jw.key( "bundleSymbolicName" );
+        jw.value( bundle.getSymbolicName() );
+    }
+
+
+    private void writeJSON(final Writer pw, final ServiceReference service, 
final Locale locale, final String filter) throws IOException
+    {
+        writeJSON( pw, service, false, locale, filter );
+    }
+
+
+    private void writeJSON( final Writer pw, final ServiceReference service, 
final boolean fullDetails, final Locale locale, final String filter )
+            throws IOException
+    {
+        final ServiceReference[] allServices = this.getServices(filter);
+        final String statusLine = getStatusLine( allServices );
+
+        final ServiceReference[] services = ( service != null ) ? new 
ServiceReference[]
+                { service } : allServices;
+
+                final JSONWriter jw = new JSONWriter( pw );
+
+                jw.object();
+
+                jw.key( "status" );
+                jw.value( statusLine );
+
+                jw.key( "serviceCount" );
+                jw.value( allServices.length );
+
+                jw.key( "data" );
+
+                jw.array();
+
+                for ( int i = 0; i < services.length; i++ )
+                {
+                    serviceInfo( jw, services[i], fullDetails || service != 
null, locale );
+                }
+
+                jw.endArray();
+
+                jw.endObject();
+
+    }
+
+
+    /**
+     * @see 
org.apache.felix.webconsole.AbstractWebConsolePlugin#doGet(javax.servlet.http.HttpServletRequest,
 javax.servlet.http.HttpServletResponse)
+     */
+    protected void doGet( HttpServletRequest request, HttpServletResponse 
response ) throws ServletException,
+    IOException
+    {
+        if (request.getPathInfo().indexOf("/res/") == -1)
+        { // not resource
+            final RequestInfo reqInfo = new RequestInfo( request );
+            if ( reqInfo.service == null && reqInfo.serviceRequested )
+            {
+                response.sendError( 404 );
+                return;
+            }
+            if ( reqInfo.extension.equals( "json" ) )
+            {
+                this.renderJSON( response, reqInfo.service, 
request.getLocale() );
+
+                // nothing more to do
+                return;
+            }
+        }
+
+        super.doGet( request, response );
+    }
+
+
+    /**
+     * @see 
org.apache.felix.webconsole.AbstractWebConsolePlugin#renderContent(javax.servlet.http.HttpServletRequest,
 javax.servlet.http.HttpServletResponse)
+     */
+    protected void renderContent( HttpServletRequest request, 
HttpServletResponse response ) throws IOException
+    {
+        // get request info from request attribute
+        final RequestInfo reqInfo = getRequestInfo( request );
+
+        final String appRoot = ( String ) request.getAttribute( 
WebConsoleConstants.ATTR_APP_ROOT );
+        StringWriter w = new StringWriter();
+        final String filter = request.getParameter(FILTER_PARAM);
+        writeJSON(w, reqInfo.service, request.getLocale(), filter);
+
+        // prepare variables
+        DefaultVariableResolver vars = ( ( DefaultVariableResolver ) 
WebConsoleUtil.getVariableResolver( request ) );
+        vars.put( "bundlePath", appRoot +  "/" + BundlesServlet.NAME + "/" );
+        vars.put( "drawDetails", String.valueOf(reqInfo.serviceRequested));
+        vars.put( "__data__", w.toString() );
+        // CVE-2025-25247 (FELIX-6751): the filter is rendered into a 
JavaScript
+        // string literal in templates/services.html, so HTML encoding is 
unsafe.
+        // Apply JavaScript-string encoding instead.
+        vars.put( "filter", filter == null ? "" : escapeJavaScript(filter));
+
+        response.getWriter().print( TEMPLATE );
+    }
+
+
+    // Replaces the original WebConsoleUtil.escapeJavaScript() implementation, 
see
+    // 
https://github.com/apache/felix-dev/blob/c916e45d4508890caf5cd486db9ff8dd8a09416c/webconsole/src/main/java/org/apache/felix/webconsole/WebConsoleUtil.java#L259
+    private static String escapeJavaScript( final String input )
+    {
+        final StringBuilder sb = new StringBuilder( input.length() + 16 );
+        for ( int i = 0; i < input.length(); i++ )
+        {
+            final char c = input.charAt( i );
+            switch ( c )
+            {
+                case '\\': sb.append( "\\\\" ); break;
+                case '\'': sb.append( "\\'" ); break;
+                case '"':  sb.append( "\\\"" ); break;
+                case '/':  sb.append( "\\/" ); break;
+                case '\n': sb.append( "\\n" ); break;
+                case '\r': sb.append( "\\r" ); break;
+                case '\t': sb.append( "\\t" ); break;
+                case '\b': sb.append( "\\b" ); break;
+                case '\f': sb.append( "\\f" ); break;
+                case '<':  sb.append( "\\u003C" ); break;
+                case '>':  sb.append( "\\u003E" ); break;
+                case '&':  sb.append( "\\u0026" ); break;
+                default:
+                    if ( c < 0x20 || c == 0x2028 || c == 0x2029 )
+                    {
+                        sb.append( String.format( "\\u%04X", (int) c ) );
+                    }
+                    else
+                    {
+                        sb.append( c );
+                    }
+            }
+        }
+        return sb.toString();
+    }
+}

Reply via email to