397535 Support pluggable alias checking to support symbolic links

This commit is contained in:
Greg Wilkins 2013-01-07 19:57:52 +11:00
parent a4c547d61f
commit ccda9bb10b
7 changed files with 322 additions and 30 deletions

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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/"));
}

View File

@ -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">

View File

@ -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
{