397535 Support pluggable alias checking to support symbolic links
This commit is contained in:
parent
a4c547d61f
commit
ccda9bb10b
|
@ -31,10 +31,13 @@ import java.util.Enumeration;
|
|||
import java.util.EventListener;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import javax.servlet.RequestDispatcher;
|
||||
import javax.servlet.Servlet;
|
||||
import javax.servlet.ServletContext;
|
||||
|
@ -140,6 +143,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server.
|
|||
private Object _requestAttributeListeners;
|
||||
private Map<String, Object> _managedAttributes;
|
||||
private String[] _protectedTargets;
|
||||
private final CopyOnWriteArrayList<AliasCheck> _aliasChecks = new CopyOnWriteArrayList<ContextHandler.AliasCheck>();
|
||||
|
||||
private boolean _shutdown = false;
|
||||
private boolean _available = true;
|
||||
|
@ -158,6 +162,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server.
|
|||
_attributes = new AttributesMap();
|
||||
_contextAttributes = new AttributesMap();
|
||||
_initParams = new HashMap<String, String>();
|
||||
addAliasCheck(new ApproveNonExistentDirectoryAliases());
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
|
@ -171,6 +176,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server.
|
|||
_attributes = new AttributesMap();
|
||||
_contextAttributes = new AttributesMap();
|
||||
_initParams = new HashMap<String, String>();
|
||||
addAliasCheck(new ApproveNonExistentDirectoryAliases());
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
|
@ -1531,14 +1537,23 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server.
|
|||
path = URIUtil.canonicalPath(path);
|
||||
Resource resource = _baseResource.addPath(path);
|
||||
|
||||
// Is the resource aliased?
|
||||
if (!_aliases && resource.getAlias() != null)
|
||||
{
|
||||
if (resource.exists())
|
||||
LOG.warn("Aliased resource: " + resource + "~=" + resource.getAlias());
|
||||
else if (path.endsWith("/") && resource.getAlias().toString().endsWith(path))
|
||||
return resource;
|
||||
else if (LOG.isDebugEnabled())
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Aliased resource: " + resource + "~=" + resource.getAlias());
|
||||
|
||||
// alias checks
|
||||
for (Iterator<AliasCheck> i=_aliasChecks.iterator();i.hasNext();)
|
||||
{
|
||||
AliasCheck check = i.next();
|
||||
if (check.check(path,resource))
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Aliased resource: " + resource + " approved by " + check);
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -1619,6 +1634,25 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server.
|
|||
|
||||
return host;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* Add an AliasCheck instance to possibly permit aliased resources
|
||||
* @param check The alias checker
|
||||
*/
|
||||
public void addAliasCheck(AliasCheck check)
|
||||
{
|
||||
_aliasChecks.add(check);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* @return Mutable list of Alias checks
|
||||
*/
|
||||
public List<AliasCheck> getAliasChecks()
|
||||
{
|
||||
return _aliasChecks;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
|
@ -2127,4 +2161,71 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server.
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/** Interface to check aliases
|
||||
*/
|
||||
public interface AliasCheck
|
||||
{
|
||||
/* ------------------------------------------------------------ */
|
||||
/** Check an alias
|
||||
* @param path The path the aliased resource was created for
|
||||
* @param resource The aliased resourced
|
||||
* @return True if the resource is OK to be served.
|
||||
*/
|
||||
boolean check(String path, Resource resource);
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/** Approve Aliases with same suffix.
|
||||
* Eg. a symbolic link from /foobar.html to /somewhere/wibble.html would be
|
||||
* approved because both the resource and alias end with ".html".
|
||||
*/
|
||||
public static class ApproveSameSuffixAliases implements AliasCheck
|
||||
{
|
||||
public boolean check(String path, Resource resource)
|
||||
{
|
||||
int dot = path.lastIndexOf('.');
|
||||
if (dot<0)
|
||||
return false;
|
||||
String suffix=path.substring(dot);
|
||||
return resource.getAlias().toString().endsWith(suffix);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/** Approve Aliases with a path prefix.
|
||||
* Eg. a symbolic link from /dirA/foobar.html to /dirB/foobar.html would be
|
||||
* approved because both the resource and alias end with "/foobar.html".
|
||||
*/
|
||||
public static class ApprovePathPrefixAliases implements AliasCheck
|
||||
{
|
||||
public boolean check(String path, Resource resource)
|
||||
{
|
||||
int slash = path.lastIndexOf('/');
|
||||
if (slash<0)
|
||||
return false;
|
||||
String suffix=path.substring(slash);
|
||||
return resource.getAlias().toString().endsWith(suffix);
|
||||
}
|
||||
}
|
||||
/* ------------------------------------------------------------ */
|
||||
/** Approve Aliases of a non existent directory.
|
||||
* If a directory "/foobar/" does not exist, then the resource is
|
||||
* aliased to "/foobar". Accept such aliases.
|
||||
*/
|
||||
public static class ApproveNonExistentDirectoryAliases implements AliasCheck
|
||||
{
|
||||
public boolean check(String path, Resource resource)
|
||||
{
|
||||
int slash = path.lastIndexOf('/');
|
||||
if (slash<0)
|
||||
return false;
|
||||
String suffix=path.substring(slash);
|
||||
return resource.getAlias().toString().endsWith(suffix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,201 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.server.handler;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FilePermission;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.FileAttribute;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import junit.framework.Assert;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.LocalConnector;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.util.resource.FileResource;
|
||||
import org.eclipse.jetty.util.resource.Resource;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* @version $Revision$
|
||||
*/
|
||||
public class ContextHandlerAliasTest
|
||||
{
|
||||
private Server _server;
|
||||
private ContextHandler _ctx;
|
||||
private File _tmp;
|
||||
private File _dir;
|
||||
|
||||
|
||||
@Before
|
||||
public void before() throws Exception
|
||||
{
|
||||
_server=new Server();
|
||||
_ctx = new ContextHandler();
|
||||
_server.setHandler(_ctx);
|
||||
|
||||
|
||||
_tmp = new File( System.getProperty( "basedir", "." ) + "/target/tmp/aliastests" ).getCanonicalFile();
|
||||
if (_tmp.exists())
|
||||
IO.delete(_tmp);
|
||||
assertTrue(_tmp.mkdirs());
|
||||
|
||||
File root = new File(_tmp,getClass().getName());
|
||||
assertTrue(root.mkdir());
|
||||
|
||||
File webInf = new File(root,"WEB-INF");
|
||||
assertTrue(webInf.mkdir());
|
||||
|
||||
assertTrue(new File(webInf,"jsp").mkdir());
|
||||
assertTrue(new File(webInf,"web.xml").createNewFile());
|
||||
assertTrue(new File(root,"index.html").createNewFile());
|
||||
|
||||
_dir=root;
|
||||
_ctx.setBaseResource(Resource.newResource(_dir));
|
||||
_server.start();
|
||||
}
|
||||
|
||||
@After
|
||||
public void after() throws Exception
|
||||
{
|
||||
_server.stop();
|
||||
if (_tmp!=null && _tmp.exists())
|
||||
IO.delete(_tmp);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetResources() throws Exception
|
||||
{
|
||||
Resource r =_ctx.getResource("/index.html");
|
||||
Assert.assertTrue(r.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testJvmNullBugAlias() throws Exception
|
||||
{
|
||||
// JVM Files ignores null characters at end of name
|
||||
String normal="/index.html";
|
||||
String withnull="/index.html\u0000";
|
||||
|
||||
_ctx.setAliases(true);
|
||||
Assert.assertTrue(_ctx.getResource(normal).exists());
|
||||
Assert.assertTrue(_ctx.getResource(withnull).exists());
|
||||
_ctx.setAliases(false);
|
||||
Assert.assertTrue(_ctx.getResource(normal).exists());
|
||||
Assert.assertNull(_ctx.getResource(withnull));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSymLinkToContext() throws Exception
|
||||
{
|
||||
File symlink = new File(_tmp,"symlink");
|
||||
try
|
||||
{
|
||||
Files.createSymbolicLink(symlink.toPath(),_dir.toPath());
|
||||
|
||||
_server.stop();
|
||||
_ctx.setBaseResource(FileResource.newResource(symlink));
|
||||
_ctx.setAliases(false);
|
||||
_server.start();
|
||||
|
||||
Resource r =_ctx.getResource("/index.html");
|
||||
Assert.assertTrue(r.exists());
|
||||
}
|
||||
finally
|
||||
{
|
||||
symlink.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSymLinkToContent() throws Exception
|
||||
{
|
||||
File symlink = new File(_dir,"link.html");
|
||||
try
|
||||
{
|
||||
Files.createSymbolicLink(symlink.toPath(),new File(_dir,"index.html").toPath());
|
||||
|
||||
_ctx.setAliases(true);
|
||||
Assert.assertTrue(_ctx.getResource("/index.html").exists());
|
||||
Assert.assertTrue(_ctx.getResource("/link.html").exists());
|
||||
|
||||
_ctx.setAliases(false);
|
||||
Assert.assertTrue(_ctx.getResource("/index.html").exists());
|
||||
Assert.assertNull(_ctx.getResource("/link.html"));
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
symlink.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSymLinkToContentWithSuffixCheck() throws Exception
|
||||
{
|
||||
File symlink = new File(_dir,"link.html");
|
||||
try
|
||||
{
|
||||
Files.createSymbolicLink(symlink.toPath(),new File(_dir,"index.html").toPath());
|
||||
|
||||
_ctx.setAliases(false);
|
||||
_ctx.addAliasCheck(new ContextHandler.ApproveSameSuffixAliases());
|
||||
Assert.assertTrue(_ctx.getResource("/index.html").exists());
|
||||
Assert.assertTrue(_ctx.getResource("/link.html").exists());
|
||||
}
|
||||
finally
|
||||
{
|
||||
symlink.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSymLinkToContentWithPathPrefixCheck() throws Exception
|
||||
{
|
||||
File symlink = new File(_dir,"dirlink");
|
||||
try
|
||||
{
|
||||
Files.createSymbolicLink(symlink.toPath(),new File(_dir,".").toPath());
|
||||
|
||||
_ctx.setAliases(false);
|
||||
_ctx.addAliasCheck(new ContextHandler.ApprovePathPrefixAliases());
|
||||
Assert.assertTrue(_ctx.getResource("/index.html").exists());
|
||||
Assert.assertTrue(_ctx.getResource("/dirlink/index.html").exists());
|
||||
}
|
||||
finally
|
||||
{
|
||||
symlink.delete();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -205,7 +205,7 @@ public class DefaultServlet extends HttpServlet implements ResourceFactory
|
|||
if (!aliases && !FileResource.getCheckAliases())
|
||||
throw new IllegalStateException("Alias checking disabled");
|
||||
if (aliases)
|
||||
_servletContext.log("Aliases are enabled");
|
||||
_servletContext.log("Aliases are enabled! Security constraints may be bypassed!!!");
|
||||
|
||||
_useFileMappedBuffer=getInitBoolean("useFileMappedBuffer",_useFileMappedBuffer);
|
||||
|
||||
|
|
|
@ -189,20 +189,6 @@ public abstract class Resource implements ResourceFactory
|
|||
}
|
||||
}
|
||||
|
||||
// Make sure that any special characters stripped really are ignorable.
|
||||
String nurl=url.toString();
|
||||
if (nurl.length()>0 && nurl.charAt(nurl.length()-1)!=resource.charAt(resource.length()-1))
|
||||
{
|
||||
if ((nurl.charAt(nurl.length()-1)!='/' ||
|
||||
nurl.charAt(nurl.length()-2)!=resource.charAt(resource.length()-1))
|
||||
&&
|
||||
(resource.charAt(resource.length()-1)!='/' ||
|
||||
resource.charAt(resource.length()-2)!=nurl.charAt(nurl.length()-1)
|
||||
))
|
||||
{
|
||||
return new BadResource(url,"Trailing special characters stripped by URL in "+resource);
|
||||
}
|
||||
}
|
||||
return newResource(url);
|
||||
}
|
||||
|
||||
|
|
|
@ -107,13 +107,8 @@ public class WebAppContextTest
|
|||
server.setHandler(context);
|
||||
server.start();
|
||||
|
||||
// When
|
||||
ServletContext ctx = context.getServletContext();
|
||||
|
||||
// Then
|
||||
// This passes:
|
||||
assertNotNull(ctx.getRealPath("/doesnotexist"));
|
||||
// This fails:
|
||||
assertNotNull(ctx.getRealPath("/doesnotexist/"));
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ detected.
|
|||
|
||||
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
|
||||
|
||||
|
||||
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
|
||||
<!-- Required minimal context configuration : -->
|
||||
<!-- + contextPath -->
|
||||
|
@ -30,6 +29,19 @@ detected.
|
|||
<Set name="defaultsDescriptor"><SystemProperty name="jetty.home" default="."/>/etc/webdefault.xml</Set>
|
||||
<Set name="overrideDescriptor"><SystemProperty name="jetty.home" default="."/>/contexts/test.d/override-web.xml</Set>
|
||||
|
||||
<!-- Allow directory symbolic links -->
|
||||
<Call name="addAliasCheck">
|
||||
<Arg>
|
||||
<New class="org.eclipse.jetty.server.handler.ContextHandler$ApprovePathPrefixAliases"/>
|
||||
</Arg>
|
||||
</Call>
|
||||
<!-- Allow file symbolic links -->
|
||||
<Call name="addAliasCheck">
|
||||
<Arg>
|
||||
<New class="org.eclipse.jetty.server.handler.ContextHandler$ApproveSameSuffixAliases"/>
|
||||
</Arg>
|
||||
</Call>
|
||||
|
||||
<!-- virtual hosts
|
||||
<Set name="virtualHosts">
|
||||
<Array type="String">
|
||||
|
|
|
@ -75,7 +75,7 @@ public class JspMatchingTest
|
|||
|
||||
// add default servlet
|
||||
ServletHolder defaultServHolder = context.addServlet(DefaultServlet.class,"/");
|
||||
defaultServHolder.setInitParameter("aliases","true"); // important!
|
||||
defaultServHolder.setInitParameter("aliases","false"); // important!
|
||||
|
||||
// add jsp
|
||||
ServletHolder jsp = context.addServlet(JspServlet.class,"*.jsp");
|
||||
|
@ -143,7 +143,6 @@ public class JspMatchingTest
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("DefaultServlet + aliasing breaks this test ATM")
|
||||
@Test
|
||||
public void testGetBeanRefInvalid_nullx() throws Exception
|
||||
{
|
||||
|
@ -164,7 +163,6 @@ public class JspMatchingTest
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("DefaultServlet + aliasing breaks this test ATM")
|
||||
@Test
|
||||
public void testGetBeanRefInvalid_nullslash() throws Exception
|
||||
{
|
||||
|
@ -185,7 +183,6 @@ public class JspMatchingTest
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("DefaultServlet + aliasing breaks this test ATM")
|
||||
@Test
|
||||
public void testGetBeanRefInvalid_nullxslash() throws Exception
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue