From feb83a1c8fd2e7670b244d5afd23cba5aca43284 Mon Sep 17 00:00:00 2001
From: cpovirk <cpovirk@google.com>
Date: Thu, 25 May 2023 13:18:00 -0700
Subject: [PATCH] Restrict permissions when creating temporary files and
 directories, or fail if that's not possible.

(Also, check that the provided `fileThreshold` is non-negative.)

- Fixes https://github.com/google/guava/issues/2575
- Fixes https://github.com/google/guava/issues/4011

RELNOTES=Reimplemented `Files.createTempDir` and `FileBackedOutputStream` to further address [CVE-2020-8908](https://github.com/google/guava/issues/4011) and [Guava issue #2575](https://github.com/google/guava/issues/2575) (CVE forthcoming).
PiperOrigin-RevId: 535359233

Refer: https://github.com/google/guava/commit/feb83a1c8fd2e7670b244d5afd23cba5aca43284
---
 .../common/io/FileBackedOutputStreamTest.java |  27 +++
 .../common/io/FilesCreateTempDirTest.java     |  41 +++-
 .../common/io/FileBackedOutputStream.java     |  22 ++-
 guava/src/com/google/common/io/Files.java     |  51 ++---
 .../common/io/IgnoreJRERequirement.java       |  30 +++
 .../com/google/common/io/TempFileCreator.java | 176 ++++++++++++++++++
 6 files changed, 302 insertions(+), 45 deletions(-)
 create mode 100644 guava/src/com/google/common/io/IgnoreJRERequirement.java
 create mode 100644 guava/src/com/google/common/io/TempFileCreator.java

diff --git a/guava-tests/test/com/google/common/io/FileBackedOutputStreamTest.java b/guava-tests/test/com/google/common/io/FileBackedOutputStreamTest.java
index 6841a41..2dae0ed 100644
--- a/guava-tests/test/com/google/common/io/FileBackedOutputStreamTest.java
+++ b/guava-tests/test/com/google/common/io/FileBackedOutputStreamTest.java
@@ -17,10 +17,18 @@
 package com.google.common.io;
 
 
+import static com.google.common.base.StandardSystemProperty.JAVA_IO_TMPDIR;
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
+import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
+import static org.junit.Assert.assertThrows;
+
 import com.google.common.testing.GcFinalization;
 import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.nio.file.attribute.PosixFileAttributeView;
+import java.nio.file.attribute.PosixFileAttributes;
 import java.util.Arrays;
 
 /**
@@ -61,10 +69,21 @@ public class FileBackedOutputStreamTest extends IoTestCase {
 
     // Write data to go over the threshold
     if (chunk2 > 0) {
+      if (JAVA_IO_TMPDIR.value().equals("/sdcard")) {
+        assertThrows(IOException.class, () -> write(out, data, chunk1, chunk2, singleByte));
+        return;
+      }
       write(out, data, chunk1, chunk2, singleByte);
       file = out.getFile();
       assertEquals(dataSize, file.length());
       assertTrue(file.exists());
+      assertThat(file.getName()).contains("FileBackedOutputStream");
+      if (!isAndroid()) {
+        PosixFileAttributes attributes =
+            java.nio.file.Files.getFileAttributeView(file.toPath(), PosixFileAttributeView.class)
+                .readAttributes();
+        assertThat(attributes.permissions()).containsExactly(OWNER_READ, OWNER_WRITE);
+      }
     }
     out.close();
 
@@ -133,6 +152,10 @@ public class FileBackedOutputStreamTest extends IoTestCase {
     FileBackedOutputStream out = new FileBackedOutputStream(50);
     ByteSource source = out.asByteSource();
 
+    if (JAVA_IO_TMPDIR.value().equals("/sdcard")) {
+      assertThrows(IOException.class, () -> out.write(data));
+      return;
+    }
     out.write(data);
     assertTrue(Arrays.equals(data, source.read()));
 
@@ -164,4 +187,8 @@ public class FileBackedOutputStreamTest extends IoTestCase {
 
     out.close();
   }
+
+  private static boolean isAndroid() {
+    return System.getProperty("java.runtime.name", "").contains("Android");
+  }
 }
diff --git a/guava-tests/test/com/google/common/io/FilesCreateTempDirTest.java b/guava-tests/test/com/google/common/io/FilesCreateTempDirTest.java
index 557689e..62098ef 100644
--- a/guava-tests/test/com/google/common/io/FilesCreateTempDirTest.java
+++ b/guava-tests/test/com/google/common/io/FilesCreateTempDirTest.java
@@ -16,9 +16,18 @@
 
 package com.google.common.io;
 
+import static com.google.common.base.StandardSystemProperty.JAVA_IO_TMPDIR;
 import static com.google.common.truth.Truth.assertThat;
+import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE;
+import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
+import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
+import static org.junit.Assert.assertThrows;
 
 import java.io.File;
+import java.io.IOException;
+import java.nio.file.attribute.PosixFileAttributeView;
+import java.nio.file.attribute.PosixFileAttributes;
+import junit.framework.TestCase;
 
 /**
  * Unit test for {@link Files#createTempDir}.
@@ -26,12 +35,32 @@ import java.io.File;
  * @author Chris Nokleberg
  */
 
-public class FilesCreateTempDirTest extends IoTestCase {
-  public void testCreateTempDir() {
+@SuppressWarnings("deprecation") // tests of a deprecated method
+public class FilesCreateTempDirTest extends TestCase {
+  public void testCreateTempDir() throws IOException {
+    if (JAVA_IO_TMPDIR.value().equals("/sdcard")) {
+      assertThrows(IllegalStateException.class, Files::createTempDir);
+      return;
+    }
     File temp = Files.createTempDir();
-    assertTrue(temp.exists());
-    assertTrue(temp.isDirectory());
-    assertThat(temp.listFiles()).isEmpty();
-    assertTrue(temp.delete());
+    try {
+      assertTrue(temp.exists());
+      assertTrue(temp.isDirectory());
+      assertThat(temp.listFiles()).isEmpty();
+
+      if (isAndroid()) {
+        return;
+      }
+      PosixFileAttributes attributes =
+          java.nio.file.Files.getFileAttributeView(temp.toPath(), PosixFileAttributeView.class)
+              .readAttributes();
+      assertThat(attributes.permissions()).containsExactly(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE);
+    } finally {
+      assertTrue(temp.delete());
+    }
+  }
+
+  private static boolean isAndroid() {
+    return System.getProperty("java.runtime.name", "").contains("Android");
   }
 }
diff --git a/guava/src/com/google/common/io/FileBackedOutputStream.java b/guava/src/com/google/common/io/FileBackedOutputStream.java
index 9912e2f..ef553a0 100644
--- a/guava/src/com/google/common/io/FileBackedOutputStream.java
+++ b/guava/src/com/google/common/io/FileBackedOutputStream.java
@@ -14,6 +14,7 @@
 
 package com.google.common.io;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.Beta;
@@ -34,6 +35,14 @@ import javax.annotation.CheckForNull;
  * An {@link OutputStream} that starts buffering to a byte array, but switches to file buffering
  * once the data reaches a configurable size.
  *
+ * <p>When this stream creates a temporary file, it restricts the file's permissions to the current
+ * user or, in the case of Android, the current app. If that is not possible (as is the case under
+ * the very old Android Ice Cream Sandwich release), then this stream throws an exception instead of
+ * creating a file that would be more accessible. (This behavior is new in Guava 32.0.0. Previous
+ * versions would create a file that is more accessible, as discussed in <a
+ * href="https://github.com/google/guava/issues/2575">Guava issue 2575</a>. TODO: b/283778848 - Fill
+ * in CVE number once it's available.)
+ *
  * <p>Temporary files created by this stream may live in the local filesystem until either:
  *
  * <ul>
@@ -57,7 +66,6 @@ public final class FileBackedOutputStream extends OutputStream {
   private final int fileThreshold;
   private final boolean resetOnFinalize;
   private final ByteSource source;
-  @CheckForNull private final File parentDirectory;
 
   @GuardedBy("this")
   private OutputStream out;
@@ -93,6 +101,7 @@ public final class FileBackedOutputStream extends OutputStream {
    * {@link ByteSource} returned by {@link #asByteSource} is finalized.
    *
    * @param fileThreshold the number of bytes before the stream should switch to buffering to a file
+   * @throws IllegalArgumentException if {@code fileThreshold} is negative
    */
   public FileBackedOutputStream(int fileThreshold) {
     this(fileThreshold, false);
@@ -105,16 +114,13 @@ public final class FileBackedOutputStream extends OutputStream {
    * @param fileThreshold the number of bytes before the stream should switch to buffering to a file
    * @param resetOnFinalize if true, the {@link #reset} method will be called when the {@link
    *     ByteSource} returned by {@link #asByteSource} is finalized.
+   * @throws IllegalArgumentException if {@code fileThreshold} is negative
    */
   public FileBackedOutputStream(int fileThreshold, boolean resetOnFinalize) {
-    this(fileThreshold, resetOnFinalize, null);
-  }
-
-  private FileBackedOutputStream(
-      int fileThreshold, boolean resetOnFinalize, @CheckForNull File parentDirectory) {
+    checkArgument(
+        fileThreshold >= 0, "fileThreshold must be non-negative, but was %s", fileThreshold);
     this.fileThreshold = fileThreshold;
     this.resetOnFinalize = resetOnFinalize;
-    this.parentDirectory = parentDirectory;
     memory = new MemoryOutput();
     out = memory;
 
@@ -225,7 +231,7 @@ public final class FileBackedOutputStream extends OutputStream {
   @GuardedBy("this")
   private void update(int len) throws IOException {
     if (memory != null && (memory.getCount() + len > fileThreshold)) {
-      File temp = File.createTempFile("FileBackedOutputStream", null, parentDirectory);
+      File temp = TempFileCreator.INSTANCE.createTempFile("FileBackedOutputStream");
       if (resetOnFinalize) {
         // Finalizers are not guaranteed to be called on system shutdown;
         // this is insurance.
diff --git a/guava/src/com/google/common/io/Files.java b/guava/src/com/google/common/io/Files.java
index ba5528f..2bc67ab 100644
--- a/guava/src/com/google/common/io/Files.java
+++ b/guava/src/com/google/common/io/Files.java
@@ -70,9 +70,6 @@ import org.checkerframework.checker.nullness.qual.Nullable;
 @ElementTypesAreNonnullByDefault
 public final class Files {
 
-  /** Maximum loop count when creating temp directories. */
-  private static final int TEMP_DIR_ATTEMPTS = 10000;
-
   private Files() {}
 
   /**
@@ -394,17 +391,19 @@ public final class Files {
    * Atomically creates a new directory somewhere beneath the system's temporary directory (as
    * defined by the {@code java.io.tmpdir} system property), and returns its name.
    *
+   * <p>The temporary directory is created with permissions restricted to the current user or, in
+   * the case of Android, the current app. If that is not possible (as is the case under the very
+   * old Android Ice Cream Sandwich release), then this method throws an exception instead of
+   * creating a directory that would be more accessible. (This behavior is new in Guava 32.0.0.
+   * Previous versions would create a directory that is more accessible, as discussed in <a
+   * href="https://github.com/google/guava/issues/4011">CVE-2020-8908</a>.)
+   *
    * <p>Use this method instead of {@link File#createTempFile(String, String)} when you wish to
    * create a directory, not a regular file. A common pitfall is to call {@code createTempFile},
    * delete the file and create a directory in its place, but this leads a race condition which can
    * be exploited to create security vulnerabilities, especially when executable files are to be
    * written into the directory.
    *
-   * <p>Depending on the environmment that this code is run in, the system temporary directory (and
-   * thus the directory this method creates) may be more visible that a program would like - files
-   * written to this directory may be read or overwritten by hostile programs running on the same
-   * machine.
-   *
    * <p>This method assumes that the temporary volume is writable, has free inodes and free blocks,
    * and that it will not be called thousands of times per second.
    *
@@ -413,35 +412,25 @@ public final class Files {
    *
    * @return the newly-created directory
    * @throws IllegalStateException if the directory could not be created
+   * @throws UnsupportedOperationException if the system does not support creating temporary
+   *     directories securely
    * @deprecated For Android users, see the <a
    *     href="https://developer.android.com/training/data-storage" target="_blank">Data and File
    *     Storage overview</a> to select an appropriate temporary directory (perhaps {@code
-   *     context.getCacheDir()}). For developers on Java 7 or later, use {@link
-   *     java.nio.file.Files#createTempDirectory}, transforming it to a {@link File} using {@link
-   *     java.nio.file.Path#toFile() toFile()} if needed.
+   *     context.getCacheDir()}), and create your own directory under that. (For example, you might
+   *     use {@code new File(context.getCacheDir(), "directoryname").mkdir()}, or, if you need an
+   *     arbitrary number of temporary directories, you might have to generate multiple directory
+   *     names in a loop until {@code mkdir()} returns {@code true}.) For developers on Java 7 or
+   *     later, use {@link java.nio.file.Files#createTempDirectory}, transforming it to a {@link
+   *     File} using {@link java.nio.file.Path#toFile() toFile()} if needed. To restrict permissions
+   *     as this method does, pass {@code
+   *     PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))} to your
+   *     call to {@code createTempDirectory}.
    */
   @Beta
   @Deprecated
   public static File createTempDir() {
-    File baseDir = new File(System.getProperty("java.io.tmpdir"));
-    @SuppressWarnings("GoodTime") // reading system time without TimeSource
-    String baseName = System.currentTimeMillis() + "-";
-
-    for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) {
-      File tempDir = new File(baseDir, baseName + counter);
-      if (tempDir.mkdir()) {
-        return tempDir;
-      }
-    }
-    throw new IllegalStateException(
-        "Failed to create directory within "
-            + TEMP_DIR_ATTEMPTS
-            + " attempts (tried "
-            + baseName
-            + "0 to "
-            + baseName
-            + (TEMP_DIR_ATTEMPTS - 1)
-            + ')');
+    return TempFileCreator.INSTANCE.createTempDir();
   }
 
   /**
diff --git a/guava/src/com/google/common/io/IgnoreJRERequirement.java b/guava/src/com/google/common/io/IgnoreJRERequirement.java
new file mode 100644
index 0000000..b1b8e10
--- /dev/null
+++ b/guava/src/com/google/common/io/IgnoreJRERequirement.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 The Guava Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.common.io;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+
+import java.lang.annotation.Target;
+
+/**
+ * Disables Animal Sniffer's checking of compatibility with older versions of Java/Android.
+ *
+ * <p>Each package's copy of this annotation needs to be listed in our {@code pom.xml}.
+ */
+@Target({METHOD, CONSTRUCTOR, TYPE})
+@ElementTypesAreNonnullByDefault
+@interface IgnoreJRERequirement {}
diff --git a/guava/src/com/google/common/io/TempFileCreator.java b/guava/src/com/google/common/io/TempFileCreator.java
new file mode 100644
index 0000000..a28a0af
--- /dev/null
+++ b/guava/src/com/google/common/io/TempFileCreator.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2007 The Guava Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.common.io;
+
+import static com.google.common.base.StandardSystemProperty.JAVA_IO_TMPDIR;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.common.annotations.J2ktIncompatible;
+import com.google.j2objc.annotations.J2ObjCIncompatible;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.Set;
+
+/**
+ * Creates temporary files and directories whose permissions are restricted to the current user or,
+ * in the case of Android, the current app. If that is not possible (as is the case under the very
+ * old Android Ice Cream Sandwich release), then this class throws an exception instead of creating
+ * a file or directory that would be more accessible.
+ */
+@J2ktIncompatible
+@GwtIncompatible
+@J2ObjCIncompatible
+@ElementTypesAreNonnullByDefault
+abstract class TempFileCreator {
+  static final TempFileCreator INSTANCE = pickSecureCreator();
+
+  /**
+   * @throws IllegalStateException if the directory could not be created (to implement the contract
+   *     of {@link Files#createTempDir()}
+   * @throws UnsupportedOperationException if the system does not support creating temporary
+   *     directories securely
+   */
+  abstract File createTempDir();
+
+  abstract File createTempFile(String prefix) throws IOException;
+
+  private static TempFileCreator pickSecureCreator() {
+    try {
+      Class.forName("java.nio.file.Path");
+      return new JavaNioCreator();
+    } catch (ClassNotFoundException runningUnderAndroid) {
+      // Try another way.
+    }
+
+    try {
+      int version = (int) Class.forName("android.os.Build$VERSION").getField("SDK_INT").get(null);
+      int jellyBean =
+          (int) Class.forName("android.os.Build$VERSION_CODES").getField("JELLY_BEAN").get(null);
+      /*
+       * I assume that this check can't fail because JELLY_BEAN will be present only if we're
+       * running under Jelly Bean or higher. But it seems safest to check.
+       */
+      if (version < jellyBean) {
+        return new ThrowingCreator();
+      }
+
+      // Don't merge these catch() blocks, let alone use ReflectiveOperationException directly:
+      // b/65343391
+    } catch (NoSuchFieldException e) {
+      // The JELLY_BEAN field doesn't exist because we're running on a version before Jelly Bean :)
+      return new ThrowingCreator();
+    } catch (ClassNotFoundException e) {
+      // Should be impossible, but we want to return *something* so that class init succeeds.
+      return new ThrowingCreator();
+    } catch (IllegalAccessException e) {
+      // ditto
+      return new ThrowingCreator();
+    }
+
+    // Android isolates apps' temporary directories since Jelly Bean:
+    // https://github.com/google/guava/issues/4011#issuecomment-770020802
+    // So we can create files there with any permissions and still get security from the isolation.
+    return new JavaIoCreator();
+  }
+
+  @IgnoreJRERequirement // used only when Path is available
+  private static final class JavaNioCreator extends TempFileCreator {
+    private static final FileAttribute<Set<PosixFilePermission>> RWX_USER_ONLY =
+        PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"));
+    private static final FileAttribute<Set<PosixFilePermission>> RW_USER_ONLY =
+        PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------"));
+
+    @Override
+    File createTempDir() {
+      try {
+        return java.nio.file.Files.createTempDirectory(
+                Paths.get(JAVA_IO_TMPDIR.value()), /* prefix= */ null, RWX_USER_ONLY)
+            .toFile();
+      } catch (IOException e) {
+        throw new IllegalStateException("Failed to create directory", e);
+      }
+    }
+
+    @Override
+    File createTempFile(String prefix) throws IOException {
+      return java.nio.file.Files.createTempFile(
+              Paths.get(JAVA_IO_TMPDIR.value()),
+              /* prefix= */ prefix,
+              /* suffix= */ null,
+              RW_USER_ONLY)
+          .toFile();
+    }
+  }
+
+  private static final class JavaIoCreator extends TempFileCreator {
+    @Override
+    File createTempDir() {
+      File baseDir = new File(JAVA_IO_TMPDIR.value());
+      @SuppressWarnings("GoodTime") // reading system time without TimeSource
+      String baseName = System.currentTimeMillis() + "-";
+
+      for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) {
+        File tempDir = new File(baseDir, baseName + counter);
+        if (tempDir.mkdir()) {
+          return tempDir;
+        }
+      }
+      throw new IllegalStateException(
+          "Failed to create directory within "
+              + TEMP_DIR_ATTEMPTS
+              + " attempts (tried "
+              + baseName
+              + "0 to "
+              + baseName
+              + (TEMP_DIR_ATTEMPTS - 1)
+              + ')');
+    }
+
+    @Override
+    File createTempFile(String prefix) throws IOException {
+      return File.createTempFile(
+          /* prefix= */ prefix,
+          /* suffix= */ null,
+          /* directory= */ null /* defaults to java.io.tmpdir */);
+    }
+
+    /** Maximum loop count when creating temp directories. */
+    private static final int TEMP_DIR_ATTEMPTS = 10000;
+  }
+
+  private static final class ThrowingCreator extends TempFileCreator {
+    private static final String MESSAGE =
+        "Guava cannot securely create temporary files or directories under SDK versions before"
+            + " Jelly Bean. You can create one yourself, either in the insecure default directory"
+            + " or in a more secure directory, such as context.getCacheDir(). For more information,"
+            + " see the Javadoc for Files.createTempDir().";
+
+    @Override
+    File createTempDir() {
+      throw new IllegalStateException(MESSAGE);
+    }
+
+    @Override
+    File createTempFile(String prefix) throws IOException {
+      throw new IOException(MESSAGE);
+    }
+  }
+
+  private TempFileCreator() {}
+}
-- 
2.41.0

