JETTY-980 & JETTY-1004

git-svn-id: svn+ssh://dev.eclipse.org/svnroot/rt/org.eclipse.jetty/jetty/trunk@199 7e9141cc-0065-0410-87d8-b60c137991c4
This commit is contained in:
Greg Wilkins 2009-04-30 03:23:13 +00:00
parent 79edb1ebee
commit 456a00161b
9 changed files with 414 additions and 34 deletions

View File

@ -1,9 +1,11 @@
jetty-7.0.0.M2-SNAPSHOT
+ JETTY-941 Linux chkconfig hint
+ JETTY-959 CGI servlet doesn't kill the CGI in case the client disconnects
+ JETTY-959 CGI servlet doesn't kill the CGI in case the client disconnects
+ JETTY-980 Fixed ResourceHandler ? handling, and bad URI creation in listings
+ JETTY-996 Make start-stop-daemon optional
+ 273767 Update to use geronimo annotations spec 1.1.1
+ JETTY-1003 java.lang.IllegalArgumentException: timeout can't be negative
+ JETTY-1004 Canonical path handling includes ? in path segment
jetty-7.0.0.M1 22 April 2009
+ 271258 FORM Authentication dispatch handling avoids caching

View File

@ -23,6 +23,7 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.resource.Resource;
/* ------------------------------------------------------------ */
@ -47,9 +48,10 @@ public class FileServer
response.getWriter().println(listing);
}
};
resource_handler.setWelcomeFiles(new String[]{"index.html"});
resource_handler.setResourceBase(args.length==2?args[1]:".");
Log.info("serving "+resource_handler.getBaseResource());
HandlerList handlers = new HandlerList();
handlers.setHandlers(new Handler[]{resource_handler,new DefaultHandler()});
server.setHandler(handlers);

View File

@ -32,6 +32,7 @@ import org.eclipse.jetty.http.HttpGenerator;
import org.eclipse.jetty.http.HttpHeaderValues;
import org.eclipse.jetty.http.HttpHeaders;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersions;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.io.BufferCache.CachedBuffer;
@ -419,12 +420,12 @@ public class Response implements HttpServletResponse
{
StringBuilder buf = _connection.getRequest().getRootURL();
if (location.startsWith("/"))
buf.append(URIUtil.canonicalPath(location));
buf.append(location);
else
{
String path=_connection.getRequest().getRequestURI();
String parent=(path.endsWith("/"))?path:URIUtil.parentPath(path);
location=URIUtil.canonicalPath(URIUtil.addPaths(parent,location));
location=URIUtil.addPaths(parent,location);
if(location==null)
throw new IllegalStateException("path cannot be above root");
if (!location.startsWith("/"))
@ -433,6 +434,27 @@ public class Response implements HttpServletResponse
}
location=buf.toString();
HttpURI uri = new HttpURI(location);
String path=uri.getDecodedPath();
String canonical=URIUtil.canonicalPath(path);
if (canonical==null)
throw new IllegalArgumentException();
if (!canonical.equals(path))
{
buf = _connection.getRequest().getRootURL();
buf.append(canonical);
if (uri.getQuery()!=null)
{
buf.append('?');
buf.append(uri.getQuery());
}
if (uri.getFragment()!=null)
{
buf.append('#');
buf.append(uri.getFragment());
}
location=buf.toString();
}
}
resetBuffer();

View File

@ -36,6 +36,7 @@ import org.eclipse.jetty.server.handler.ContextHandler.Context;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.resource.FileResource;
import org.eclipse.jetty.util.resource.Resource;
@ -56,6 +57,7 @@ public class ResourceHandler extends AbstractHandler
String[] _welcomeFiles={"index.html"};
MimeTypes _mimeTypes = new MimeTypes();
ByteArrayBuffer _cacheControl;
boolean _aliases;
/* ------------------------------------------------------------ */
public ResourceHandler()
@ -74,12 +76,41 @@ public class ResourceHandler extends AbstractHandler
_mimeTypes = mimeTypes;
}
/* ------------------------------------------------------------ */
/**
* @return True if resource aliases are allowed.
*/
public boolean isAliases()
{
return _aliases;
}
/* ------------------------------------------------------------ */
/**
* Set if resource aliases (eg symlink, 8.3 names, case insensitivity) are allowed.
* Allowing aliases can significantly increase security vulnerabilities.
* If this handler is deployed inside a ContextHandler, then the
* {@link ContextHandler#isAliases()} takes precedent.
* @param aliases True if aliases are supported.
*/
public void setAliases(boolean aliases)
{
_aliases = aliases;
}
/* ------------------------------------------------------------ */
public void doStart()
throws Exception
{
Context scontext = ContextHandler.getCurrentContext();
_context = (scontext==null?null:scontext.getContextHandler());
if (_context!=null)
_aliases=_context.isAliases();
if (!_aliases && !FileResource.getCheckAliases())
throw new IllegalStateException("Alias checking disabled");
super.doStart();
}
@ -239,6 +270,11 @@ public class ResourceHandler extends AbstractHandler
if (resource==null || !resource.exists())
return;
if (!_aliases && resource.getAlias()!=null)
{
Log.info(resource+" aliased to "+resource.getAlias());
return;
}
// We are going to server something
base_request.setHandled(true);

View File

@ -52,6 +52,7 @@ import org.eclipse.jetty.util.MultiPartOutputStream;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.resource.FileResource;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
@ -154,9 +155,13 @@ public class DefaultServlet extends HttpServlet implements ResourceFactory
_redirectWelcome=getInitBoolean("redirectWelcome",_redirectWelcome);
_gzip=getInitBoolean("gzip",_gzip);
String aliases=_servletContext.getInitParameter("aliases");
if (aliases!=null)
_contextHandler.setAliases(Boolean.parseBoolean(aliases));
if (getInitParameter("aliases")!=null)
_contextHandler.setAliases(getInitBoolean("aliases",false));
boolean aliases=_contextHandler.isAliases();
if (!aliases && !FileResource.getCheckAliases())
throw new IllegalStateException("Alias checking disabled");
if (aliases)
_servletContext.log("Aliases are enabled");
_useFileMappedBuffer=getInitBoolean("useFileMappedBuffer",_useFileMappedBuffer);

View File

@ -1,12 +1,16 @@
package org.eclipse.jetty.servlet;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.IO;
public class DefaultServletTest extends TestCase
{
@ -42,37 +46,278 @@ public class DefaultServletTest extends TestCase
server.stop();
}
}
public void testListingXSS() throws Exception
public void testListingWithSession() throws Exception
{
ServletHolder defholder = context.addServlet(DefaultServlet.class,"/listing/*");
ServletHolder defholder = context.addServlet(DefaultServlet.class,"/*");
defholder.setInitParameter("dirAllowed","true");
defholder.setInitParameter("redirectWelcome","false");
defholder.setInitParameter("gzip","false");
File resBase = new File("src/test/resources");
assertTrue("resBase.exists",resBase.exists());
assertTrue("resBase.isDirectory",resBase.isDirectory());
File testDir = new File("target/tests/" + getName());
prepareEmptyTestDir(testDir);
/* create some content in the docroot */
File resBase = new File(testDir, "docroot");
resBase.mkdirs();
new File(resBase, "one").mkdir();
new File(resBase, "two").mkdir();
new File(resBase, "three").mkdir();
String resBasePath = resBase.getAbsolutePath();
defholder.setInitParameter("resourceBase",resBasePath);
StringBuffer req1 = new StringBuffer();
req1.append("GET /context/listing/;<script>window.alert(\"hi\");</script> HTTP/1.1\n");
req1.append("GET /context/;JSESSIONID=1234567890 HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("Connection: close\n");
req1.append("\n");
String response = connector.getResponses(req1.toString());
assertResponseContains("listing/one/",response);
assertResponseContains("listing/two/",response);
assertResponseContains("listing/three/",response);
assertResponseContains("/one/;JSESSIONID=1234567890",response);
assertResponseContains("/two/;JSESSIONID=1234567890",response);
assertResponseContains("/three/;JSESSIONID=1234567890",response);
assertResponseNotContains("<script>",response);
}
public void testListingXSS() throws Exception
{
ServletHolder defholder = context.addServlet(DefaultServlet.class,"/*");
defholder.setInitParameter("dirAllowed","true");
defholder.setInitParameter("redirectWelcome","false");
defholder.setInitParameter("gzip","false");
File testDir = new File("target/tests/" + getName());
prepareEmptyTestDir(testDir);
/* create some content in the docroot */
File resBase = new File(testDir, "docroot");
resBase.mkdirs();
new File(resBase, "one").mkdir();
new File(resBase, "two").mkdir();
new File(resBase, "three").mkdir();
assertTrue("Creating dir 'f??r' (Might not work in Windows)", new File(resBase, "f??r").mkdir());
String resBasePath = resBase.getAbsolutePath();
defholder.setInitParameter("resourceBase",resBasePath);
StringBuffer req1 = new StringBuffer();
/* Intentionally bad request URI.
* Sending a non-encoded URI with typically encoded characters '<', '>', and '"'.
*/
req1.append("GET /context/;<script>window.alert(\"hi\");</script> HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("\n");
String response = connector.getResponses(req1.toString());
assertResponseContains("/one/",response);
assertResponseContains("/two/",response);
assertResponseContains("/three/",response);
assertResponseContains("/f%3F%3Fr",response);
assertResponseNotContains("<script>",response);
}
public void testListingProperUrlEncoding() throws Exception
{
ServletHolder defholder = context.addServlet(DefaultServlet.class,"/*");
defholder.setInitParameter("dirAllowed","true");
defholder.setInitParameter("redirectWelcome","false");
defholder.setInitParameter("gzip","false");
File testDir = new File("target/tests/" + getName());
prepareEmptyTestDir(testDir);
/* create some content in the docroot */
File resBase = new File(testDir, "docroot");
resBase.mkdirs();
File wackyDir = new File(resBase, "dir;"); // this should not be double-encoded.
assertTrue(wackyDir.mkdirs());
new File(wackyDir, "four").mkdir();
new File(wackyDir, "five").mkdir();
new File(wackyDir, "six").mkdir();
/* At this point we have the following
* testListingProperUrlEncoding/
* `-- docroot
* `-- dir;
* |-- five
* |-- four
* `-- six
*/
String resBasePath = resBase.getAbsolutePath();
defholder.setInitParameter("resourceBase",resBasePath);
// First send request in improper, unencoded way.
String response = connector.getResponses("GET /context/dir;/ HTTP/1.0\r\n\r\n");
assertResponseContains("HTTP/1.1 404 Not Found", response);
// Now send request in proper, encoded format.
connector.reopen();
StringBuffer req2 = new StringBuffer();
response = connector.getResponses("GET /context/dir%3B/ HTTP/1.0\r\n\r\n");
// Should not see double-encoded ";"
// First encoding: ";" -> "%3b"
// Second encoding: "%3B" -> "%253B" (BAD!)
assertResponseNotContains("%253B",response);
assertResponseContains("/dir%3B/",response);
assertResponseContains("/dir%3B/four/",response);
assertResponseContains("/dir%3B/five/",response);
assertResponseContains("/dir%3B/six/",response);
}
public void testListingContextBreakout() throws Exception
{
ServletHolder defholder = context.addServlet(DefaultServlet.class,"/*");
defholder.setInitParameter("dirAllowed","true");
defholder.setInitParameter("redirectWelcome","false");
defholder.setInitParameter("gzip","false");
File testDir = new File("target/tests/" + getName());
prepareEmptyTestDir(testDir);
/* create some content in the docroot */
File resBase = new File(testDir, "docroot");
resBase.mkdirs();
new File(resBase, "one").mkdir();
new File(resBase, "two").mkdir();
new File(resBase, "three").mkdir();
File wackyDir = new File(resBase, "dir");
assertTrue(wackyDir.mkdirs());
new File(wackyDir, "four").mkdir();
new File(wackyDir, "five").mkdir();
new File(wackyDir, "six").mkdir();
wackyDir = new File(resBase, "dir;");
assertTrue(wackyDir.mkdirs());
/* create some content outside of the docroot */
File sekret = new File(testDir, "sekret");
sekret.mkdirs();
File pass = new File(sekret, "pass");
createFile(pass, "Sssh, you shouldn't be seeing this");
/* At this point we have the following
* testListingContextBreakout/
* |-- docroot
* | |-- dir
* | | |-- five
* | | |-- four
* | | `-- six
* | |-- dir;
* | |-- one
* | |-- three
* | `-- two
* `-- sekret
* `-- pass
*/
String resBasePath = resBase.getAbsolutePath();
defholder.setInitParameter("resourceBase",resBasePath);
String response = connector.getResponses("GET /context/dir/?/../../sekret/pass HTTP/1.0\r\n\r\n");
assertResponseContains("/four/",response);
assertResponseContains("/five/",response);
assertResponseContains("/six/",response);
assertResponseNotContains("Sssh",response);
connector.reopen();
response = connector.getResponses("GET /context/dir/../../../sekret/pass HTTP/1.0\r\n\r\n");
assertResponseNotContains("Sssh",response);
connector.reopen();
response = connector.getResponses("GET /context/dir/%3F/../../sekret/pass HTTP/1.0\r\n\r\n");
assertResponseNotContains("Sssh",response);
connector.reopen();
response = connector.getResponses("GET /context/dir/%3F/../../../sekret/pass HTTP/1.0\r\n\r\n");
assertResponseNotContains("Sssh",response);
connector.reopen();
response = connector.getResponses("GET /context/dir%3F/../../sekret/pass HTTP/1.0\r\n\r\n");
assertResponseNotContains("Sssh",response);
connector.reopen();
response = connector.getResponses("GET /context/dir%3B/ HTTP/1.0\r\n\r\n");
assertResponseContains("Directory: /context/dir;/<",response);
connector.reopen();
response = connector.getResponses("GET /context/dir/../ HTTP/1.0\r\n\r\n");
assertResponseContains("Directory: /context/<",response);
connector.reopen();
response = connector.getResponses("GET /context/dir%3B/../ HTTP/1.0\r\n\r\n");
assertResponseContains("Directory: /context/<",response);
connector.reopen();
response = connector.getResponses("GET /context/dir%3B/../../sekret/pass HTTP/1.0\r\n\r\n");
assertResponseContains("Not Found",response);
}
private void createFile(File file, String str) throws IOException
{
FileOutputStream out = null;
try {
out = new FileOutputStream(file);
out.write(str.getBytes());
out.flush();
} finally {
IO.close(out);
}
}
private void prepareEmptyTestDir(File testdir)
{
if (testdir.exists())
{
emptyDir(testdir);
}
else
{
testdir.mkdirs();
}
assertTrue("test dir should exists",testdir.exists());
assertTrue("test dir should be a dir",testdir.isDirectory());
assertTrue("test dir should be empty",isEmpty(testdir));
}
private boolean isEmpty(File dir)
{
if (!dir.isDirectory())
{
return true;
}
return dir.list().length == 0;
}
private void emptyDir(File dir)
{
File entries[] = dir.listFiles();
for (int i = 0; i < entries.length; i++)
{
deletePath(entries[i]);
}
}
private void deletePath(File path)
{
if (path.isDirectory())
{
File entries[] = path.listFiles();
for (int i = 0; i < entries.length; i++)
{
deletePath(entries[i]);
}
}
assertTrue("Deleting: " + path.getAbsolutePath(),path.delete());
}
private void assertResponseNotContains(String forbidden, String response)
{
int idx = response.indexOf(forbidden);

View File

@ -304,8 +304,8 @@ public class URIUtil
/** Add two URI path segments.
* Handles null and empty paths, path and query params (eg ?a=b or
* ;JSESSIONID=xxx) and avoids duplicate '/'
* @param p1 URI path segment
* @param p2 URI path segment
* @param p1 URI path segment (should be encoded)
* @param p2 URI path segment (should be encoded)
* @return Legally combined path segments.
*/
public static String addPaths(String p1, String p2)
@ -395,8 +395,10 @@ public class URIUtil
return path;
int end=path.length();
int queryIdx=path.indexOf('?');
int start = path.lastIndexOf('/', (queryIdx > 0 ? queryIdx : end));
// int start = path.lastIndexOf('/', end);
search:
while (end>0)

View File

@ -49,7 +49,9 @@ public class FileResource extends URLResource
(System.getProperty("org.eclipse.jetty.util.FileResource.checkAliases","true"));
if (__checkAliases)
Log.debug("Checking Resource aliases");
Log.debug("Checking Resource aliases");
else
Log.warn("Resource alias checking is disabled");
}
/* ------------------------------------------------------------ */

View File

@ -416,11 +416,11 @@ public abstract class Resource implements Serializable
* @param parent True if the parent directory should be included
* @return String of HTML
*/
public String getListHTML(String base,
boolean parent)
public String getListHTML(String base,boolean parent)
throws IOException
{
if (!isDirectory())
base=URIUtil.canonicalPath(base);
if (base==null || !isDirectory())
return null;
String[] ls = list();
@ -436,28 +436,32 @@ public abstract class Resource implements Serializable
buf.append(title);
buf.append("</TITLE></HEAD><BODY>\n<H1>");
buf.append(title);
buf.append("</H1><TABLE BORDER=0>");
buf.append("</H1>\n<TABLE BORDER=0>\n");
if (parent)
{
buf.append("<TR><TD><A HREF=\"");
URIUtil.encodePath(buf,URIUtil.addPaths(base,"../"));
buf.append(URIUtil.addPaths(base,"../"));
buf.append("\">Parent Directory</A></TD><TD></TD><TD></TD></TR>\n");
}
String defangedBase = defangURI(base);
DateFormat dfmt=DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
DateFormat.MEDIUM);
for (int i=0 ; i< ls.length ; i++)
{
String encoded=URIUtil.encodePath(ls[i]);
Resource item = addPath(ls[i]);
buf.append("<TR><TD><A HREF=\"");
String path=URIUtil.addPaths(base,encoded);
buf.append("\n<TR><TD><A HREF=\"");
String path=URIUtil.addPaths(defangedBase,URIUtil.encodePath(ls[i]));
buf.append(path);
if (item.isDirectory() && !path.endsWith("/"))
path=URIUtil.addPaths(path,URIUtil.SLASH);
URIUtil.encodePath(buf,path);
buf.append(URIUtil.SLASH);
// URIUtil.encodePath(buf,path);
buf.append("\">");
buf.append(deTag(ls[i]));
buf.append("&nbsp;");
@ -465,7 +469,7 @@ public abstract class Resource implements Serializable
buf.append(item.length());
buf.append(" bytes&nbsp;</TD><TD>");
buf.append(dfmt.format(new Date(item.lastModified())));
buf.append("</TD></TR>\n");
buf.append("</TD></TR>");
}
buf.append("</TABLE>\n");
buf.append("</BODY></HTML>\n");
@ -473,7 +477,67 @@ public abstract class Resource implements Serializable
return buf.toString();
}
private static String deTag(String raw) {
/**
* Defang any characters that could break the URI string in an HREF.
* Such as <a href="/path/to;<script>Window.alert("XSS"+'%20'+"here");</script>">Link</a>
*
* The above example would parse incorrectly on various browsers as the "<" or '"' characters
* would end the href attribute value string prematurely.
*
* @param raw the raw text to defang.
* @return the defanged text.
*/
private static String defangURI(String raw)
{
StringBuffer buf = null;
if (buf==null)
{
for (int i=0;i<raw.length();i++)
{
char c=raw.charAt(i);
switch(c)
{
case '\'':
case '"':
case '<':
case '>':
buf=new StringBuffer(raw.length()<<1);
break;
}
}
if (buf==null)
return raw;
}
for (int i=0;i<raw.length();i++)
{
char c=raw.charAt(i);
switch(c)
{
case '"':
buf.append("%22");
continue;
case '\'':
buf.append("%27");
continue;
case '<':
buf.append("%3C");
continue;
case '>':
buf.append("%3E");
continue;
default:
buf.append(c);
continue;
}
}
return buf.toString();
}
private static String deTag(String raw)
{
return StringUtil.replace( StringUtil.replace(raw,"<","&lt;"), ">", "&gt;");
}