/** * Secure recursive delete using {@code SecureDirectoryStream}. Returns a collection of * exceptions that occurred or null if no exceptions were thrown. */ @Nullable private static Collection<IOException> deleteRecursivelySecure( SecureDirectoryStream<Path> dir, Path path) { Collection<IOException> exceptions = null; try { if (isDirectory(dir, path, NOFOLLOW_LINKS)) { try (SecureDirectoryStream<Path> childDir = dir.newDirectoryStream(path, NOFOLLOW_LINKS)) { exceptions = deleteDirectoryContentsSecure(childDir); } // If exceptions is not null, something went wrong trying to delete the contents of the // directory, so we shouldn't try to delete the directory as it will probably fail. if (exceptions == null) { dir.deleteDirectory(path); } } else { dir.deleteFile(path); } return exceptions; } catch (IOException e) { return addException(exceptions, e); } }
@Override public void move( Path srcpath, SecureDirectoryStream<Path> targetdir, Path targetpath) throws IOException { EphemeralFsPath efsSrcPath = cast(srcpath); EphemeralFsPath efsTargetPath = cast(targetpath); EphemeralFsSecureDirectoryStream efsTargetDir = cast(targetdir); synchronized(efsSrcPath.fs.fsLock) { EphemeralFsPath actualSrcPath = translate(efsSrcPath); EphemeralFsPath actualTargetPath = efsTargetDir.translate(efsTargetPath); efsSrcPath.fs.move(actualSrcPath, actualTargetPath, new CopyOption[] {StandardCopyOption.ATOMIC_MOVE}); } }
/** * Secure recursive delete using {@code SecureDirectoryStream}. Returns a collection of exceptions * that occurred or null if no exceptions were thrown. */ @NullableDecl private static Collection<IOException> deleteRecursivelySecure( SecureDirectoryStream<Path> dir, Path path) { Collection<IOException> exceptions = null; try { if (isDirectory(dir, path, NOFOLLOW_LINKS)) { try (SecureDirectoryStream<Path> childDir = dir.newDirectoryStream(path, NOFOLLOW_LINKS)) { exceptions = deleteDirectoryContentsSecure(childDir); } // If exceptions is not null, something went wrong trying to delete the contents of the // directory, so we shouldn't try to delete the directory as it will probably fail. if (exceptions == null) { dir.deleteDirectory(path); } } else { dir.deleteFile(path); } return exceptions; } catch (IOException e) { return addException(exceptions, e); } }
@Override public void move(Path srcPath, SecureDirectoryStream<Path> targetDir, Path targetPath) throws IOException { checkOpen(); JimfsPath checkedSrcPath = checkPath(srcPath); JimfsPath checkedTargetPath = checkPath(targetPath); if (!(targetDir instanceof JimfsSecureDirectoryStream)) { throw new ProviderMismatchException( "targetDir isn't a secure directory stream associated with this file system"); } JimfsSecureDirectoryStream checkedTargetDir = (JimfsSecureDirectoryStream) targetDir; view.copy( checkedSrcPath, checkedTargetDir.view, checkedTargetPath, ImmutableSet.<CopyOption>of(), true); }
@Test public void testSecureDirectoryStreamBasedOnRelativePath() throws IOException { Files.createDirectories(path("foo")); Files.createFile(path("foo/a")); Files.createFile(path("foo/b")); Files.createDirectory(path("foo/c")); Files.createFile(path("foo/c/d")); Files.createFile(path("foo/c/e")); try (DirectoryStream<Path> stream = Files.newDirectoryStream(path("foo"))) { SecureDirectoryStream<Path> secureStream = (SecureDirectoryStream<Path>) stream; assertThat(ImmutableList.copyOf(secureStream)) .containsExactly(path("foo/a"), path("foo/b"), path("foo/c")); try (DirectoryStream<Path> stream2 = secureStream.newDirectoryStream(path("c"))) { assertThat(ImmutableList.copyOf(stream2)).containsExactly(path("foo/c/d"), path("foo/c/e")); } } }
/** * Deletes the file or directory at the given {@code path} recursively. Deletes symbolic links, * not their targets (subject to the caveat below). * * <p>If an I/O exception occurs attempting to read, open or delete any file under the given * directory, this method skips that file and continues. All such exceptions are collected and, * after attempting to delete all files, an {@code IOException} is thrown containing those * exceptions as {@linkplain Throwable#getSuppressed() suppressed exceptions}. * * <h2>Warning: Security of recursive deletes</h2> * * <p>On a file system that supports symbolic links and does <i>not</i> support * {@link SecureDirectoryStream}, it is possible for a recursive delete to delete files and * directories that are <i>outside</i> the directory being deleted. This can happen if, after * checking that a file is a directory (and not a symbolic link), that directory is replaced by a * symbolic link to an outside directory before the call that opens the directory to read its * entries. * * <p>By default, this method throws {@link InsecureRecursiveDeleteException} if it can't * guarantee the security of recursive deletes. If you wish to allow the recursive deletes * anyway, pass {@link RecursiveDeleteOption#ALLOW_INSECURE} to this method to override that * behavior. * * @throws NoSuchFileException if {@code path} does not exist <i>(optional specific * exception)</i> * @throws InsecureRecursiveDeleteException if the security of recursive deletes can't be * guaranteed for the file system and {@link RecursiveDeleteOption#ALLOW_INSECURE} was not * specified * @throws IOException if {@code path} or any file in the subtree rooted at it can't be deleted * for any reason */ public static void deleteRecursively( Path path, RecursiveDeleteOption... options) throws IOException { Path parentPath = getParentPath(path); if (parentPath == null) { throw new FileSystemException(path.toString(), null, "can't delete recursively"); } Collection<IOException> exceptions = null; // created lazily if needed try { boolean sdsSupported = false; try (DirectoryStream<Path> parent = Files.newDirectoryStream(parentPath)) { if (parent instanceof SecureDirectoryStream) { sdsSupported = true; exceptions = deleteRecursivelySecure( (SecureDirectoryStream<Path>) parent, path.getFileName()); } } if (!sdsSupported) { checkAllowsInsecure(path, options); exceptions = deleteRecursivelyInsecure(path); } } catch (IOException e) { if (exceptions == null) { throw e; } else { exceptions.add(e); } } if (exceptions != null) { throwDeleteFailed(path, exceptions); } }
/** * Secure method for deleting the contents of a directory using {@code SecureDirectoryStream}. * Returns a collection of exceptions that occurred or null if no exceptions were thrown. */ @Nullable private static Collection<IOException> deleteDirectoryContentsSecure( SecureDirectoryStream<Path> dir) { Collection<IOException> exceptions = null; try { for (Path path : dir) { exceptions = concat(exceptions, deleteRecursivelySecure(dir, path.getFileName())); } return exceptions; } catch (DirectoryIteratorException e) { return addException(exceptions, e.getCause()); } }
/** * Returns whether or not the file with the given name in the given dir is a directory. */ private static boolean isDirectory( SecureDirectoryStream<Path> dir, Path name, LinkOption... options) throws IOException { return dir.getFileAttributeView(name, BasicFileAttributeView.class, options) .readAttributes() .isDirectory(); }
@Override public SecureDirectoryStream<Path> newDirectoryStream(Path path, LinkOption... options) throws IOException { checkClosed(); MCRPath mcrPath = checkFileSystem(path); if (mcrPath.isAbsolute()) { return (SecureDirectoryStream<Path>) Files.newDirectoryStream(mcrPath); } MCRFilesystemNode childByPath = dir.getChildByPath(mcrPath.toString()); if (childByPath == null || childByPath instanceof MCRFile) { throw new NoSuchFileException(dir.toString(), path.toString(), "Does not exist or is a file."); } return new MCRDirectoryStream((MCRDirectory) childByPath, MCRPath.toMCRPath(path.resolve(mcrPath))); }
@Override public void move(Path srcpath, SecureDirectoryStream<Path> targetdir, Path targetpath) throws IOException { checkClosed(); checkFileSystem(srcpath); checkFileSystem(targetpath); throw new AtomicMoveNotSupportedException(srcpath.toString(), targetpath.toString(), "Currently not implemented"); }
@Override public SecureDirectoryStream<Path> newDirectoryStream(Path path, LinkOption... options) throws IOException { EphemeralFsPath efsPath = cast(path); synchronized(efsPath.fs.fsLock) { EphemeralFsPath actualPath = translate(efsPath); for(LinkOption option : options) { if(option == LinkOption.NOFOLLOW_LINKS) { ResolvedPath resolved = ResolvedPath.resolve(actualPath, true); if(resolved.resolvedToSymbolicLink()) { throw new FileSystemException(path + ": Too many levels of symbolic links"); } } } return (SecureDirectoryStream<Path>) actualPath.fs.newDirectoryStream( actualPath, efsPath.isAbsolute() ? efsPath : myPath.resolve(efsPath), new Filter<Path>() { @Override public boolean accept(Path entry) throws IOException { return true; } } ); } }
private EphemeralFsSecureDirectoryStream cast(SecureDirectoryStream<Path> p) { if(!(p instanceof EphemeralFsSecureDirectoryStream)) { throw new IllegalStateException("wrong file system:" + p); } EphemeralFsSecureDirectoryStream answer = (EphemeralFsSecureDirectoryStream) p; if(answer.myPath.fs != myPath.fs) { throw new IllegalStateException("wrong fs"); } return answer; }
@Before public void setUp() throws IOException { dir = root.resolve("dir"); Files.createDirectories(dir); fixture = (SecureDirectoryStream<Path>) Files.newDirectoryStream(dir); moveTo = root.resolve("moveTo"); Files.move(dir, moveTo); }
@Test public void testNewDirectoryStreamAfterMove() throws IOException { Files.createFile(moveTo.resolve("newFile")); try(SecureDirectoryStream<Path> newStream = fixture.newDirectoryStream(root.getFileSystem().getPath("."))) { assertFound(newStream, dir.resolve(".").resolve("newFile")); } }
@Test public void testNewDirectoryStreamAfterMoveFailsSymlink() throws IOException { Files.createDirectory(moveTo.resolve("newDir")); Files.createSymbolicLink(moveTo.resolve("link"), moveTo.resolve("newDir")); try(SecureDirectoryStream<Path> newStream = fixture.newDirectoryStream(root.getFileSystem().getPath("link"), LinkOption.NOFOLLOW_LINKS)) { fail(); } catch(FileSystemException e) { assertTrue( e.getMessage(), e.getMessage().contains("Too many levels of symbolic links")); } }
@Test public void testNewDirectoryStreamAfterMoveMultiLevel() throws IOException { Files.createDirectory(moveTo.resolve("child")); Files.createFile(moveTo.resolve("child").resolve("newFile")); try(SecureDirectoryStream<Path> newStream = fixture.newDirectoryStream(root.getFileSystem().getPath("child"))) { assertFound(newStream, dir.resolve("child").resolve("newFile")); } }
@Test public void testDirectoryStreamIsNotSecure() throws Exception { try(DirectoryStream<Path> stream = Files.newDirectoryStream(root)) { assertFalse(stream instanceof SecureDirectoryStream); } }
/** * Deletes the file or directory at the given {@code path} recursively. Deletes symbolic links, * not their targets (subject to the caveat below). * * <p>If an I/O exception occurs attempting to read, open or delete any file under the given * directory, this method skips that file and continues. All such exceptions are collected and, * after attempting to delete all files, an {@code IOException} is thrown containing those * exceptions as {@linkplain Throwable#getSuppressed() suppressed exceptions}. * * <h2>Warning: Security of recursive deletes</h2> * * <p>On a file system that supports symbolic links and does <i>not</i> support {@link * SecureDirectoryStream}, it is possible for a recursive delete to delete files and directories * that are <i>outside</i> the directory being deleted. This can happen if, after checking that a * file is a directory (and not a symbolic link), that directory is replaced by a symbolic link to * an outside directory before the call that opens the directory to read its entries. * * <p>By default, this method throws {@link InsecureRecursiveDeleteException} if it can't * guarantee the security of recursive deletes. If you wish to allow the recursive deletes anyway, * pass {@link RecursiveDeleteOption#ALLOW_INSECURE} to this method to override that behavior. * * @throws NoSuchFileException if {@code path} does not exist <i>(optional specific exception)</i> * @throws InsecureRecursiveDeleteException if the security of recursive deletes can't be * guaranteed for the file system and {@link RecursiveDeleteOption#ALLOW_INSECURE} was not * specified * @throws IOException if {@code path} or any file in the subtree rooted at it can't be deleted * for any reason */ public static void deleteRecursively(Path path, RecursiveDeleteOption... options) throws IOException { Path parentPath = getParentPath(path); if (parentPath == null) { throw new FileSystemException(path.toString(), null, "can't delete recursively"); } Collection<IOException> exceptions = null; // created lazily if needed try { boolean sdsSupported = false; try (DirectoryStream<Path> parent = Files.newDirectoryStream(parentPath)) { if (parent instanceof SecureDirectoryStream) { sdsSupported = true; exceptions = deleteRecursivelySecure((SecureDirectoryStream<Path>) parent, path.getFileName()); } } if (!sdsSupported) { checkAllowsInsecure(path, options); exceptions = deleteRecursivelyInsecure(path); } } catch (IOException e) { if (exceptions == null) { throw e; } else { exceptions.add(e); } } if (exceptions != null) { throwDeleteFailed(path, exceptions); } }
/** * Secure method for deleting the contents of a directory using {@code SecureDirectoryStream}. * Returns a collection of exceptions that occurred or null if no exceptions were thrown. */ @NullableDecl private static Collection<IOException> deleteDirectoryContentsSecure( SecureDirectoryStream<Path> dir) { Collection<IOException> exceptions = null; try { for (Path path : dir) { exceptions = concat(exceptions, deleteRecursivelySecure(dir, path.getFileName())); } return exceptions; } catch (DirectoryIteratorException e) { return addException(exceptions, e.getCause()); } }
/** Returns whether or not the file with the given name in the given dir is a directory. */ private static boolean isDirectory( SecureDirectoryStream<Path> dir, Path name, LinkOption... options) throws IOException { return dir.getFileAttributeView(name, BasicFileAttributeView.class, options) .readAttributes() .isDirectory(); }
@Override public SecureDirectoryStream<Path> newDirectoryStream(Path path, LinkOption... options) throws IOException { checkOpen(); JimfsPath checkedPath = checkPath(path); // safe cast because a file system that supports SecureDirectoryStream always creates // SecureDirectoryStreams return (SecureDirectoryStream<Path>) view.newDirectoryStream( checkedPath, ALWAYS_TRUE_FILTER, Options.getLinkOptions(options), path().resolve(checkedPath)); }
/** * Deletes all files within the directory at the given {@code path} * {@linkplain #deleteRecursively recursively}. Does not delete the directory itself. Deletes * symbolic links, not their targets (subject to the caveat below). If {@code path} itself is * a symbolic link to a directory, that link is followed and the contents of the directory it * targets are deleted. * * <p>If an I/O exception occurs attempting to read, open or delete any file under the given * directory, this method skips that file and continues. All such exceptions are collected and, * after attempting to delete all files, an {@code IOException} is thrown containing those * exceptions as {@linkplain Throwable#getSuppressed() suppressed exceptions}. * * <h2>Warning: Security of recursive deletes</h2> * * <p>On a file system that supports symbolic links and does <i>not</i> support * {@link SecureDirectoryStream}, it is possible for a recursive delete to delete files and * directories that are <i>outside</i> the directory being deleted. This can happen if, after * checking that a file is a directory (and not a symbolic link), that directory is replaced by a * symbolic link to an outside directory before the call that opens the directory to read its * entries. * * <p>By default, this method throws {@link InsecureRecursiveDeleteException} if it can't * guarantee the security of recursive deletes. If you wish to allow the recursive deletes * anyway, pass {@link RecursiveDeleteOption#ALLOW_INSECURE} to this method to override that * behavior. * * @throws NoSuchFileException if {@code path} does not exist <i>(optional specific * exception)</i> * @throws NotDirectoryException if the file at {@code path} is not a directory <i>(optional * specific exception)</i> * @throws InsecureRecursiveDeleteException if the security of recursive deletes can't be * guaranteed for the file system and {@link RecursiveDeleteOption#ALLOW_INSECURE} was not * specified * @throws IOException if one or more files can't be deleted for any reason */ public static void deleteDirectoryContents( Path path, RecursiveDeleteOption... options) throws IOException { Collection<IOException> exceptions = null; // created lazily if needed try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) { if (stream instanceof SecureDirectoryStream) { SecureDirectoryStream<Path> sds = (SecureDirectoryStream<Path>) stream; exceptions = deleteDirectoryContentsSecure(sds); } else { checkAllowsInsecure(path, options); exceptions = deleteDirectoryContentsInsecure(stream); } } catch (IOException e) { if (exceptions == null) { throw e; } else { exceptions.add(e); } } if (exceptions != null) { throwDeleteFailed(path, exceptions); } }
public static void runTest(Path path) throws Exception { // check temporary file has been deleted after jvm termination if (Files.exists(path)) { throw new RuntimeException("Temporary file was not deleted"); } // check temporary file has been deleted after closing it Path file = Files.createTempFile("blep", "tmp"); Files.newByteChannel(file, READ, WRITE, DELETE_ON_CLOSE).close(); if (Files.exists(file)) throw new RuntimeException("Temporary file was not deleted"); Path dir = Files.createTempDirectory("blah"); try { // check that DELETE_ON_CLOSE fails when file is a sym link if (TestUtil.supportsLinks(dir)) { file = dir.resolve("foo"); Files.createFile(file); Path link = dir.resolve("link"); Files.createSymbolicLink(link, file); try { Files.newByteChannel(link, READ, WRITE, DELETE_ON_CLOSE); throw new RuntimeException("IOException expected"); } catch (IOException ignore) { } } // check that DELETE_ON_CLOSE works with files created via open // directories try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) { if (stream instanceof SecureDirectoryStream) { SecureDirectoryStream<Path> secure = (SecureDirectoryStream<Path>)stream; file = Paths.get("foo"); Set<OpenOption> opts = new HashSet<>(); opts.add(WRITE); opts.add(DELETE_ON_CLOSE); secure.newByteChannel(file, opts).close(); if (Files.exists(dir.resolve(file))) throw new RuntimeException("File not deleted"); } } } finally { TestUtil.removeAll(dir); } }
/** * Deletes all files within the directory at the given {@code path} {@linkplain #deleteRecursively * recursively}. Does not delete the directory itself. Deletes symbolic links, not their targets * (subject to the caveat below). If {@code path} itself is a symbolic link to a directory, that * link is followed and the contents of the directory it targets are deleted. * * <p>If an I/O exception occurs attempting to read, open or delete any file under the given * directory, this method skips that file and continues. All such exceptions are collected and, * after attempting to delete all files, an {@code IOException} is thrown containing those * exceptions as {@linkplain Throwable#getSuppressed() suppressed exceptions}. * * <h2>Warning: Security of recursive deletes</h2> * * <p>On a file system that supports symbolic links and does <i>not</i> support {@link * SecureDirectoryStream}, it is possible for a recursive delete to delete files and directories * that are <i>outside</i> the directory being deleted. This can happen if, after checking that a * file is a directory (and not a symbolic link), that directory is replaced by a symbolic link to * an outside directory before the call that opens the directory to read its entries. * * <p>By default, this method throws {@link InsecureRecursiveDeleteException} if it can't * guarantee the security of recursive deletes. If you wish to allow the recursive deletes anyway, * pass {@link RecursiveDeleteOption#ALLOW_INSECURE} to this method to override that behavior. * * @throws NoSuchFileException if {@code path} does not exist <i>(optional specific exception)</i> * @throws NotDirectoryException if the file at {@code path} is not a directory <i>(optional * specific exception)</i> * @throws InsecureRecursiveDeleteException if the security of recursive deletes can't be * guaranteed for the file system and {@link RecursiveDeleteOption#ALLOW_INSECURE} was not * specified * @throws IOException if one or more files can't be deleted for any reason */ public static void deleteDirectoryContents(Path path, RecursiveDeleteOption... options) throws IOException { Collection<IOException> exceptions = null; // created lazily if needed try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) { if (stream instanceof SecureDirectoryStream) { SecureDirectoryStream<Path> sds = (SecureDirectoryStream<Path>) stream; exceptions = deleteDirectoryContentsSecure(sds); } else { checkAllowsInsecure(path, options); exceptions = deleteDirectoryContentsInsecure(stream); } } catch (IOException e) { if (exceptions == null) { throw e; } else { exceptions.add(e); } } if (exceptions != null) { throwDeleteFailed(path, exceptions); } }
DowngradedDirectoryStream(SecureDirectoryStream<Path> secureDirectoryStream) { this.secureDirectoryStream = checkNotNull(secureDirectoryStream); }