mirror of
https://github.com/jenkinsci/jenkins.git
synced 2026-10-29 04:25:23 +00:00
[SECURITY-1452]
This commit is contained in:
1 parent
f576b2eb43
commit
71d2ecf1a4
10 files changed
+1899
-114
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user