Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@


import java.net.URL;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
Expand Down Expand Up @@ -202,19 +204,53 @@ protected final String[] getCssReferences()
* resources are accessed like <code>/system/console/abc/res/logo.gif</code>,
* the code here will try load resource <code>/res/logo.gif</code> from the
* bundle, providing the plugin.
* Path segments are normalized before the resource is loaded, so a request
* cannot escape the plugin's resource directory using <code>..</code>
* segments.
*
*
* @param path the path to read.
* @return the URL of the resource or <code>null</code> if not found.
*/
protected URL getResource( String path )
{
return ( path != null && path.startsWith( labelRes ) ) ? //
getClass().getResource( path.substring( labelResLen ) )
final String normalizedPath = path == null ? null : normalizePath(path);
return ( normalizedPath != null && normalizedPath.startsWith( labelRes ) ) ? //
getClass().getResource( normalizedPath.substring( labelResLen ) )
: null;
}


/**
* Normalizes an absolute resource path without applying host filesystem
* semantics. Resource names always use forward slashes, irrespective of
* the operating system hosting the web console.
*/
private static String normalizePath(final String path)
{
final Deque<String> segments = new ArrayDeque<>();
for (final String segment : path.substring(1).split("/"))
{
if (segment.isEmpty() || ".".equals(segment))
{
continue;
}
if ("..".equals(segment))
{
if (!segments.isEmpty())
{
segments.removeLast();
}
}
else
{
segments.addLast(segment);
}
}
return '/' + String.join("/", segments);
}


// -- begin methods for plugin registration/unregistration
/**
* This is an utility method. It is used to register the plugin service. Don't
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayDeque;
import java.util.Deque;

import org.apache.felix.webconsole.internal.Util;

Expand Down Expand Up @@ -74,19 +76,47 @@ public abstract class AbstractServlet extends HttpServlet {
* Called to identify resources.
* By default, if the path starts with "/res/" this is treated as a resource
* and the URL to the resource is tried to be loaded via the class loader.
* Path segments are normalized before this check, so a request cannot escape
* the {@code /res/} directory using {@code ..} segments.
* @param path the path
* @return the URL of the resource or <code>null</code> if not found.
*/
protected URL getResource( final String path ) {
if (path == null) {
return null;
}
final int index = path.indexOf( '/', 1 );
if (index != -1) {
if (path.substring(index).startsWith("/res/") ) {
return getClass().getResource( path.substring(index) );
final String resourcePath = normalizePath(path.substring(index));
if (resourcePath.startsWith("/res/") ) {
return getClass().getResource(resourcePath);
}
}
return null;
}

/**
* Normalizes an absolute resource path without applying host filesystem
* semantics. Resource names always use forward slashes, irrespective of
* the operating system hosting the web console.
*/
private static String normalizePath(final String path) {
final Deque<String> segments = new ArrayDeque<>();
for (final String segment : path.substring(1).split("/")) {
if (segment.isEmpty() || ".".equals(segment)) {
continue;
}
if ("..".equals(segment)) {
if (!segments.isEmpty()) {
segments.removeLast();
}
} else {
segments.addLast(segment);
}
}
return '/' + String.join("/", segments);
}

/**
* Handle get requests. This method can be used to return resources, like JSON responses etc.
* If the plugin is serving a resource, this method call {@link HttpServletResponse#setStatus(int)}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ public void test_getGetResourceMethod_extension_class() throws Exception
assertNull( getGetResourceMethod.invoke( testPrivate, (Object[]) null ) );
}


public void test_getResource_normalizesPath() {
final SimpleTestPlugin plugin = new SimpleTestPlugin();

assertNotNull(plugin.getResource("/test/res/ui/webconsole.css"));
assertNotNull(plugin.getResource("/test/res/ui/../ui/webconsole.css"));
assertNull(plugin.getResource("/test/res/../../META-INF/MANIFEST.MF"));
}

private static class PrivateTestPlugin extends TestPlugin
{
@SuppressWarnings("unused")
Expand Down Expand Up @@ -175,4 +184,18 @@ protected void renderContent( HttpServletRequest req, HttpServletResponse res )
}

}

private static class SimpleTestPlugin extends SimpleWebConsolePlugin
{
SimpleTestPlugin()
{
super("test", "Test", null);
}

@Override
protected void renderContent(final HttpServletRequest req, final HttpServletResponse res)
{
// nothing to render
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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.servlet;

import java.net.URL;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import junit.framework.TestCase;

public class AbstractServletTest extends TestCase {

public void testGetResourceNormalizesPath() {
final TestServlet servlet = new TestServlet();

assertNotNull(servlet.getResource("/test/res/ui/webconsole.css"));
assertNotNull(servlet.getResource("/test/res/ui/../ui/webconsole.css"));
assertNull(servlet.getResource("/test/res/../../META-INF/MANIFEST.MF"));
}

private static final class TestServlet extends AbstractServlet {

@Override
public void renderContent(final HttpServletRequest request, final HttpServletResponse response)
throws ServletException {
// nothing to render
}

@Override
protected URL getResource(final String path) {
return super.getResource(path);
}
}
}
Loading