[SECURITY-1452]

This commit is contained in:
Jeff Thompson authored and Jenkins CERT CI committed 2021-01-11 16:53:00 +00:00
1 parent f576b2eb43
commit 71d2ecf1a4
10 files changed
+1899 -114

No files matched your search

+257 -9
View File
@@ -83,9 +83,10 @@ import java.net.URLConnection;
import java.nio.file.FileSystemException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.LinkOption;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFilePermission;
@@ -474,6 +475,25 @@ public final class FilePath implements SerializableOnlyOverRemoting {
return archive(ArchiverFactory.ZIP, out, scanner);
}
/**
* Uses the given scanner on 'this' directory to list up files and then archive it to a zip stream.
*
* @param out The OutputStream to write the zip into.
* @param scanner A DirScanner for scanning the directory and filtering its contents.
* @param verificationRoot A root or base directory for checking for any symlinks in this files parentage.
* Any symlinks between a file and root should be ignored.
* Symlinks in the parentage outside root will not be checked.
* @param noFollowLinks true if it should not follow links.
*
* @return The number of files/directories archived.
* This is only really useful to check for a situation where nothing
*/
@Restricted(NoExternalUse.class)
public int zip(OutputStream out, DirScanner scanner, String verificationRoot, boolean noFollowLinks) throws IOException, InterruptedException {
ArchiverFactory archiverFactory = noFollowLinks ? ArchiverFactory.ZIP_WTHOUT_FOLLOWING_SYMLINKS : ArchiverFactory.ZIP;
return archive(archiverFactory, out, scanner, verificationRoot, noFollowLinks);
}
/**
* Archives this directory into the specified archive format, to the given {@link OutputStream}, by using
* {@link DirScanner} to choose what files to include.
@@ -483,23 +503,48 @@ public final class FilePath implements SerializableOnlyOverRemoting {
* is archived.
*/
public int archive(final ArchiverFactory factory, OutputStream os, final DirScanner scanner) throws IOException, InterruptedException {
return archive(factory, os, scanner, null, false);
}
/**
* Archives this directory into the specified archive format, to the given {@link OutputStream}, by using
* {@link DirScanner} to choose what files to include.
*
* @param factory The ArchiverFactory for creating the archive.
* @param os The OutputStream to write the zip into.
* @param verificationRoot A root or base directory for checking for any symlinks in this files parentage.
* Any symlinks between a file and root should be ignored.
* Symlinks in the parentage outside root will not be checked.
* @param noFollowLinks true if it should not follow links.
*
* @return The number of files/directories archived.
* This is only really useful to check for a situation where nothing
*/
@Restricted(NoExternalUse.class)
public int archive(final ArchiverFactory factory, OutputStream os, final DirScanner scanner,
String verificationRoot, boolean noFollowLinks) throws IOException, InterruptedException {
final OutputStream out = (channel!=null)?new RemoteOutputStream(os):os;
return act(new Archive(factory, out, scanner));
return act(new Archive(factory, out, scanner, verificationRoot, noFollowLinks));
}
private class Archive extends SecureFileCallable<Integer> {
private final ArchiverFactory factory;
private final OutputStream out;
private final DirScanner scanner;
Archive(ArchiverFactory factory, OutputStream out, DirScanner scanner) {
private final String verificationRoot;
private final boolean noFollowLinks;
Archive(ArchiverFactory factory, OutputStream out, DirScanner scanner, String verificationRoot, boolean noFollowLinks) {
this.factory = factory;
this.out = out;
this.scanner = scanner;
this.verificationRoot = verificationRoot;
this.noFollowLinks = noFollowLinks;
}
@Override
public Integer invoke(File f, VirtualChannel channel) throws IOException {
Archiver a = factory.create(out);
try {
scanner.scan(f,reading(a));
scanner.scan(f, ignoringSymlinks(reading(a), verificationRoot, noFollowLinks));
} finally {
a.close();
}
@@ -691,6 +736,46 @@ public final class FilePath implements SerializableOnlyOverRemoting {
}
}
@Restricted(NoExternalUse.class)
public boolean hasSymlink(FilePath verificationRoot, boolean noFollowLinks) throws IOException, InterruptedException {
return act(new HasSymlink(verificationRoot == null ? null : verificationRoot.remote, noFollowLinks));
}
private static class HasSymlink extends SecureFileCallable<Boolean> {
private static final long serialVersionUID = 1L;
private final String verificationRoot;
private final boolean noFollowLinks;
public HasSymlink(String verificationRoot, boolean noFollowLinks) {
this.verificationRoot = verificationRoot;
this.noFollowLinks = noFollowLinks;
}
public Boolean invoke(File f, VirtualChannel channel) throws IOException {
return isSymlink(f, verificationRoot, noFollowLinks);
}
}
@Restricted(NoExternalUse.class)
public boolean containsSymlink(FilePath verificationRoot, boolean noFollowLinks) throws IOException, InterruptedException {
return !list(new SymlinkRetainingFileFilter(verificationRoot, noFollowLinks)).isEmpty();
}
private static class SymlinkRetainingFileFilter implements FileFilter, Serializable {
private final String verificationRoot;
private final boolean noFollowLinks;
public SymlinkRetainingFileFilter(FilePath verificationRoot, boolean noFollowLinks) {
this.verificationRoot = verificationRoot == null ? null : verificationRoot.remote;
this.noFollowLinks = noFollowLinks;
}
public boolean accept(File file) {
return isSymlink(file, verificationRoot, noFollowLinks);
}
private static final long serialVersionUID = 1L;
}
/**
* Creates a symlink to the specified target.
*
@@ -1838,6 +1923,23 @@ public final class FilePath implements SerializableOnlyOverRemoting {
return list((FileFilter)null);
}
/**
* List up files and directories in this directory.
*
* This is intended to allow the caller to provide {@link java.nio.file.LinkOption#NOFOLLOW_LINKS} to ignore
* symlinks.
* @param verificationRoot A root or base directory for checking for any symlinks in this files parentage.
* Any symlinks between a file and root should be ignored.
* Symlinks in the parentage outside root will not be checked.
* @param noFollowLinks true if it should not follow links.
* @return Direct children of this directory.
*/
@Restricted(NoExternalUse.class)
@NonNull
public List<FilePath> list(FilePath verificationRoot, boolean noFollowLinks) throws IOException, InterruptedException {
return list(new SymlinkDiscardingFileFilter(verificationRoot, noFollowLinks));
}
/**
* List up subdirectories.
*
@@ -1982,25 +2084,112 @@ public final class FilePath implements SerializableOnlyOverRemoting {
* Reads this file.
*/
public InputStream read() throws IOException, InterruptedException {
return read(null, false);
}
@Restricted(NoExternalUse.class)
public InputStream read(FilePath rootPath, boolean noFollowLinks) throws IOException, InterruptedException {
String rootPathString = rootPath == null ? null : rootPath.remote;
if(channel==null) {
return Files.newInputStream(fileToPath(reading(new File(remote))));
File file = reading(new File(remote));
InputStream inputStream = newInputStreamDenyingSymlinkAsNeeded(file, rootPathString, noFollowLinks);
return inputStream;
}
final Pipe p = Pipe.createRemoteToLocal();
actAsync(new Read(p));
actAsync(new Read(p, rootPathString, noFollowLinks));
return p.getIn();
}
@Restricted(NoExternalUse.class)
public static InputStream newInputStreamDenyingSymlinkAsNeeded(File file, String verificationRoot, boolean noFollowLinks) throws IOException {
InputStream inputStream = null;
try {
denySymlink(file, verificationRoot, noFollowLinks);
inputStream = noFollowLinks ? Files.newInputStream(fileToPath(file), LinkOption.NOFOLLOW_LINKS) : Files.newInputStream(fileToPath(file));
denySymlink(file, verificationRoot, noFollowLinks);
} catch (IOException ioe) {
if (inputStream != null) {
inputStream.close();
}
throw ioe;
}
return inputStream;
}
private static void denySymlink(File file, String root, boolean noFollowLinks) throws IOException {
/* This should be checked right before the file is opened or otherwise traversed.
If at all possible, it should also be checked immediately afterwards.
This narrows any possible race conditions that may exist in weird situations,
platforms, or implementations.
newInputStreamDenyingSymlinkAsNeeded(...) demonstrates how this would be done.
This is useful for preventing symlink following on systems that don't support
LinkOption.NOFOLLOW_LINK. Notable among those is AIX. It is also important for
prohibiting Windows Junctions, which are not considered symlinks by the
Files.newInputStream(path, LinkOption.NOFOLLOW_LINKS) implementation.
*/
if (isSymlink(file, root, noFollowLinks)) {
throw new IOException("Symlinks are prohibited.");
}
}
@Restricted(NoExternalUse.class)
public static boolean isSymlink(File file, String root, boolean noFollowLinks) {
if (noFollowLinks) {
if (Util.isSymlink(file.toPath())) {
return true;
}
return isFileAncestorSymlink(file, root);
}
return false;
}
/**
* Determines whether an ancestor of this file is a symlink, between the specified
* file and the root path. Ancestors further up the tree are not considered.
* @param file The base file for the beginning of the search.
* @param root The root path for ending the search.
* @return True if there is a symlink within the domain. False otherwise.
*/
private static boolean isFileAncestorSymlink(File file, String root) {
if (root != null) {
Path rootPath = Paths.get(root);
Path currPath = file.toPath();
try {
while (!getRealPath(currPath).equals(getRealPath(rootPath))) {
if (Util.isSymlink(currPath)) {
return true;
}
currPath = currPath.getParent();
if (currPath == null) {
return false;
}
}
} catch (IOException ioe) {
return false;
}
}
return false;
}
private class Read extends SecureFileCallable<Void> {
private static final long serialVersionUID = 1L;
private final Pipe p;
Read(Pipe p) {
private String verificationRoot;
private boolean noFollowLinks;
Read(Pipe p, String verificationRoot, boolean noFollowLinks) {
this.p = p;
this.verificationRoot = verificationRoot;
this.noFollowLinks = noFollowLinks;
}
@Override
public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
try (InputStream fis = Files.newInputStream(fileToPath(reading(f)));
OutputStream out = p.getOut()) {
try (InputStream fis = newInputStreamDenyingSymlinkAsNeeded(reading(f), verificationRoot, noFollowLinks); OutputStream out = p.getOut()) {
org.apache.commons.io.IOUtils.copy(fis, out);
} catch (Exception x) {
p.error(x);
@@ -3249,6 +3438,29 @@ public final class FilePath implements SerializableOnlyOverRemoting {
};
}
/**
* Wraps {@link FileVisitor} to ignore symlinks.
*/
@Restricted(NoExternalUse.class)
public static FileVisitor ignoringSymlinks(final FileVisitor v, String verificationRoot, boolean noFollowLinks) {
if (noFollowLinks) {
return new FileVisitor() {
@Override
public void visit(File f, String relativePath) throws IOException {
if (verificationRoot == null || !FilePath.isSymlink(f, verificationRoot, noFollowLinks)) {
v.visit(f, relativePath);
}
}
@Override
public boolean understandsSymlink() {
return false;
}
};
}
return v;
}
/**
* Pass through 'f' after ensuring that we can read that file.
*/
@@ -3429,5 +3641,41 @@ public final class FilePath implements SerializableOnlyOverRemoting {
}
}
private static Path getRealPath(Path path) throws IOException {
return Functions.isWindows() ? windowsToRealPath(path) : path.toRealPath();
}
private static @NonNull Path windowsToRealPath(@NonNull Path path) throws IOException {
try {
return path.toRealPath();
}
catch (IOException e) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, String.format("relaxedToRealPath cannot use the regular toRealPath on %s, trying with toRealPath(LinkOption.NOFOLLOW_LINKS)", path), e);
}
}
// that's required for specific environment like Windows Server 2016, running MSFT docker
// where the root is a <SYMLINKD>
return path.toRealPath(LinkOption.NOFOLLOW_LINKS);
}
private static final SoloFilePathFilter UNRESTRICTED = SoloFilePathFilter.wrap(FilePathFilter.UNRESTRICTED);
private static class SymlinkDiscardingFileFilter implements FileFilter, Serializable {
private final String verificationRoot;
private final boolean noFollowLinks;
public SymlinkDiscardingFileFilter(FilePath verificationRoot, boolean noFollowLinks) {
this.verificationRoot = verificationRoot == null ? null : verificationRoot.remote;
this.noFollowLinks = noFollowLinks;
}
public boolean accept(File file) {
return !isSymlink(file, verificationRoot, noFollowLinks);
}
private static final long serialVersionUID = 1L;
}
}