Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #6497 - Replace the Alias checkers with new implementation. (Jetty-9.4) #6540

Closed
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
a1bb8f3
Issue #6497 - Implement the AllowedResourceAliasChecker to improve Sa…
lachlan-roberts Jul 7, 2021
32ae130
Issue #6497 - add testing for AliasCheckers with symlinks
lachlan-roberts Jul 9, 2021
be8c6ac
Issue #6497 - AllowedResourceAliasChecker can test target of symlink
lachlan-roberts Jul 12, 2021
7c0c3c9
Issue #6497 - improve testing for AliasCheck with symlinks
lachlan-roberts Jul 23, 2021
1858153
Issue #6277 - add replacement Symlink AliasChecker
lachlan-roberts Jul 23, 2021
ad85c40
Merge remote-tracking branch 'origin/jetty-9.4.x' into jetty-9.4.x-Al…
lachlan-roberts Jul 23, 2021
b916269
Add the new SymlinkAliasChecker to fix test.
lachlan-roberts Jul 26, 2021
6909476
Deprecate AllowSymLinkAliasChecker in favour of SymlinkAllowedResourc…
lachlan-roberts Jul 26, 2021
68d93fb
Issue #6497 - Improve comments, re-implement the hasSymbolicLink method.
lachlan-roberts Jul 27, 2021
a7639bc
Issue #6497 - Make AllowedResourceAliasChecker a LifeCycle
lachlan-roberts Jul 30, 2021
32bff77
Issue #6497 - Fix AllowedResourceAliasChecker if protectedTarget does…
lachlan-roberts Jul 30, 2021
bf078be
Issue #6497 - aliasChecker with no baseResource should always denies …
lachlan-roberts Jul 30, 2021
27d8069
Fix AliasChecker lifecycle issue in ContextHandlerGetResourceTest
lachlan-roberts Aug 2, 2021
6e66604
Use root path in AllowedResourceAliasChecker if no basePath is define…
lachlan-roberts Aug 2, 2021
b1fe449
Clean up symlinks to not confuse DeploymentErrorTest
lachlan-roberts Aug 3, 2021
57741c0
changes from review
lachlan-roberts Aug 26, 2021
File filter

Filter by extension

Filter by extension

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

import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AllowSymLinkAliasChecker;
import org.eclipse.jetty.server.SymlinkAllowedResourceAliasChecker;
import org.eclipse.jetty.server.session.DefaultSessionCache;
import org.eclipse.jetty.server.session.DefaultSessionIdManager;
import org.eclipse.jetty.webapp.WebAppContext;
Expand All @@ -47,7 +47,7 @@ public static void main(String[] args) throws Exception
WebAppContext webapp = new WebAppContext();
webapp.setContextPath("/");
webapp.setWar("../../jetty-distribution/target/distribution/demo-base/webapps/test.war");
webapp.addAliasCheck(new AllowSymLinkAliasChecker());
webapp.addAliasCheck(new SymlinkAllowedResourceAliasChecker(webapp));
GCloudSessionDataStore ds = new GCloudSessionDataStore();

DefaultSessionCache ss = new DefaultSessionCache(webapp.getSessionHandler());
Expand Down
57 changes: 57 additions & 0 deletions jetty-io/src/test/java/org/eclipse/jetty/io/IOTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.resource.PathResource;
import org.eclipse.jetty.util.resource.Resource;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
Expand Down Expand Up @@ -550,4 +552,59 @@ public void testSelectorWakeup() throws Exception
}
}
}

@Test
public void testSymbolicLink(TestInfo testInfo) throws Exception
{
File dir = MavenTestingUtils.getTargetTestingDir(testInfo.getDisplayName());
FS.ensureEmpty(dir);
File realFile = new File(dir, "real");
Path realPath = realFile.toPath();
FS.touch(realFile);

File linkFile = new File(dir, "link");
Path linkPath = linkFile.toPath();
Files.createSymbolicLink(linkPath, realPath);
Path targPath = linkPath.toRealPath();

System.err.printf("realPath = %s%n", realPath);
System.err.printf("linkPath = %s%n", linkPath);
System.err.printf("targPath = %s%n", targPath);

assertFalse(Files.isSymbolicLink(realPath));
assertTrue(Files.isSymbolicLink(linkPath));

Resource link = new PathResource(dir).addPath("link");
assertThat(link.isAlias(), is(true));
}

@Test
public void testSymbolicLinkDir(TestInfo testInfo) throws Exception
{
File dir = MavenTestingUtils.getTargetTestingDir(testInfo.getDisplayName());
FS.ensureEmpty(dir);

File realDirFile = new File(dir, "real");
Path realDirPath = realDirFile.toPath();
Files.createDirectories(realDirPath);

File linkDirFile = new File(dir, "link");
Path linkDirPath = linkDirFile.toPath();
Files.createSymbolicLink(linkDirPath, realDirPath);

File realFile = new File(realDirFile, "file");
Path realPath = realFile.toPath();
FS.touch(realFile);

File linkFile = new File(linkDirFile, "file");
Path linkPath = linkFile.toPath();
Path targPath = linkPath.toRealPath();

System.err.printf("realPath = %s%n", realPath);
System.err.printf("linkPath = %s%n", linkPath);
System.err.printf("targPath = %s%n", targPath);

assertFalse(Files.isSymbolicLink(realPath));
assertFalse(Files.isSymbolicLink(linkPath));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
// ------------------------------------------------------------------------
// 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;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.Objects;

import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.PathResource;
import org.eclipse.jetty.util.resource.Resource;

/**
* <p>This will approve any alias to anything inside of the {@link ContextHandler}s resource base which
* is not protected by {@link ContextHandler#isProtectedTarget(String)}.</p>
* <p>This will approve symlinks to outside of the resource base. This can be optionally configured to check that the
* target of the symlinks is also inside of the resource base and is not a protected target.</p>
* <p>Aliases approved by this may still be able to bypass SecurityConstraints, so this class would need to be extended
* to enforce any additional security constraints that are required.</p>
*/
public class AllowedResourceAliasChecker implements ContextHandler.AliasCheck
{
private static final Logger LOG = Log.getLogger(AllowedResourceAliasChecker.class);
private static final LinkOption[] FOLLOW_LINKS = new LinkOption[0];
private static final LinkOption[] NO_FOLLOW_LINKS = new LinkOption[]{LinkOption.NOFOLLOW_LINKS};

private final ContextHandler _contextHandler;
private final boolean _checkSymlinkTargets;

/**
* @param contextHandler the context handler to use.
* @param checkSymlinkTargets true to check that the target of the symlink is an allowed resource.
*/
public AllowedResourceAliasChecker(ContextHandler contextHandler, boolean checkSymlinkTargets)
{
_contextHandler = contextHandler;
lachlan-roberts marked this conversation as resolved.
Show resolved Hide resolved
_checkSymlinkTargets = checkSymlinkTargets;
}

@Override
public boolean check(String uri, Resource resource)
{
// The existence check resolves the symlinks.
if (!resource.exists())
return false;

Path resourcePath = getPath(resource);
if (resourcePath == null)
return false;

try
{
if (isProtectedPath(resourcePath, NO_FOLLOW_LINKS))
return false;

if (_checkSymlinkTargets && hasSymbolicLink(resourcePath))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this check here? Should it not be in the SymlinkAllowedResourceAliasChecker subclass?
I mean that seems weird that this base class does a bit of symlink but not all... Can't all symlink handling and configuration fields be pushed to the subclass?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a different check to what is done in the subclass, the subclass does not even use this logic, it sets _checkSymlinkTargets to false.

This is an optional check to ensure the target of the symlink is also an allowed resource.

{
if (isProtectedPath(resourcePath, FOLLOW_LINKS))
return false;
}
}
catch (Throwable t)
{
LOG.warn(t);
return false;
}

return true;
}

protected boolean isProtectedPath(Path resourcePath, LinkOption[] linkOptions) throws IOException
lachlan-roberts marked this conversation as resolved.
Show resolved Hide resolved
{
String basePath = Objects.requireNonNull(getPath(_contextHandler.getBaseResource())).toRealPath(linkOptions).toString();
String targetPath = resourcePath.toRealPath(linkOptions).toString();

if (!targetPath.startsWith(basePath))
return true;

String[] protectedTargets = _contextHandler.getProtectedTargets();
lachlan-roberts marked this conversation as resolved.
Show resolved Hide resolved
if (protectedTargets != null)
{
for (String s : protectedTargets)
{
// TODO: we are always following links for the base resource.
// We cannot use toRealPath(linkOptions) here as it throws if file does not exist,
// and the protected targets do not have to always exist.
String protectedTarget = new File(basePath, s).getCanonicalPath();
if (StringUtil.startsWithIgnoreCase(targetPath, protectedTarget))
lachlan-roberts marked this conversation as resolved.
Show resolved Hide resolved
{
if (targetPath.length() == protectedTarget.length())
return true;

// Check that the target prefix really is a path segment.
char c = targetPath.charAt(protectedTarget.length());
if (c == File.separatorChar)
return true;
}
}
}

return false;
}

protected boolean hasSymbolicLink(Path path)
lachlan-roberts marked this conversation as resolved.
Show resolved Hide resolved
{
// Is file itself a symlink?
if (Files.isSymbolicLink(path))
return true;

// Lets try each path segment
Path base = path.getRoot();
for (Path segment : path)
{
base = base.resolve(segment);
if (Files.isSymbolicLink(base))
return true;
}
lachlan-roberts marked this conversation as resolved.
Show resolved Hide resolved

return false;
}

private Path getPath(Resource resource)
{
try
{
if (resource instanceof PathResource)
return ((PathResource)resource).getPath();
return resource.getFile().toPath();
}
catch (Throwable t)
{
LOG.ignore(t);
return null;
}
}

@Override
public String toString()
{
return String.format("%s@%x{checkSymlinkTargets=%s}", AllowedResourceAliasChecker.class.getSimpleName(), hashCode(), _checkSymlinkTargets);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@
* or Linux on XFS) the the actual file could be stored using UTF-16,
* but be accessed using NFD UTF-8 or NFC UTF-8 for the same file.
* </p>
* @deprecated use {@link org.eclipse.jetty.server.AllowedResourceAliasChecker} instead.
*/
@Deprecated
public class SameFileAliasChecker implements AliasCheck
{
private static final Logger LOG = Log.getLogger(SameFileAliasChecker.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
// ------------------------------------------------------------------------
// 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;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.Resource;

/**
* An extension of {@link AllowedResourceAliasChecker} which only allows resources if they are symlinks.
lachlan-roberts marked this conversation as resolved.
Show resolved Hide resolved
*/
public class SymlinkAllowedResourceAliasChecker extends AllowedResourceAliasChecker
{
private static final Logger LOG = Log.getLogger(SymlinkAllowedResourceAliasChecker.class);

/**
* @param contextHandler the context handler to use.
*/
public SymlinkAllowedResourceAliasChecker(ContextHandler contextHandler)
{
super(contextHandler, false);
}

@Override
public boolean check(String uri, Resource resource)
{
try
{
// Check the resource is allowed to be accessed.
if (!super.check(uri, resource))
return false;

// Only approve resource if it is accessed by a symbolic link.
lachlan-roberts marked this conversation as resolved.
Show resolved Hide resolved
Path resourcePath = resource.getFile().toPath();
if (Files.isSymbolicLink(resourcePath))
return true;

// TODO: If base resource contains symlink then this will always return true.
// But we don't want to deny all paths if the resource base is symbolically linked.
lachlan-roberts marked this conversation as resolved.
Show resolved Hide resolved
if (super.hasSymbolicLink(resourcePath))
lachlan-roberts marked this conversation as resolved.
Show resolved Hide resolved
return true;
}
catch (IOException e)
{
LOG.ignore(e);
return false;
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
* to check resources that are aliased to other locations. The checker uses the
* Java {@link Files#readSymbolicLink(Path)} and {@link Path#toRealPath(java.nio.file.LinkOption...)}
* APIs to check if a file is aliased with symbolic links.</p>
* @deprecated use {@link org.eclipse.jetty.server.SymlinkAllowedResourceAliasChecker} instead.
*/
@Deprecated
gregw marked this conversation as resolved.
Show resolved Hide resolved
public class AllowSymLinkAliasChecker implements AliasCheck
{
private static final Logger LOG = Log.getLogger(AllowSymLinkAliasChecker.class);
Expand Down