holgerfriedrich commented on code in PR #2729: URL: https://github.com/apache/karaf/pull/2729#discussion_r3535298085
########## webconsole/console/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java: ########## @@ -0,0 +1,464 @@ +/* + * 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.felix.webconsole.internal.core; Review Comment: maybe add some comment here that this is the original file form 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 plus the replacement `escapeJavaScript()` ########## webconsole/console/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java: ########## @@ -0,0 +1,464 @@ +/* + * 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.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 ); + } + + + private static String escapeJavaScript( final String input ) Review Comment: replaces original implementation, see https://github.com/apache/felix-dev/blob/c916e45d4508890caf5cd486db9ff8dd8a09416c/webconsole/src/main/java/org/apache/felix/webconsole/WebConsoleUtil.java#L259 -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
