Skip to content

Commit feb83a1

Browse files
cpovirkGoogle Java Core Libraries
authored and
Google Java Core Libraries
committedMay 25, 2023
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 #2575 - Fixes #4011 RELNOTES=Reimplemented `Files.createTempDir` and `FileBackedOutputStream` to further address [CVE-2020-8908](#4011) and [Guava issue #2575](#2575) (CVE forthcoming). PiperOrigin-RevId: 535359233
1 parent 9407ed6 commit feb83a1

14 files changed

+602
-89
lines changed
 

‎android/guava-tests/test/com/google/common/io/FileBackedOutputStreamTest.java

+27
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,17 @@
1616

1717
package com.google.common.io;
1818

19+
import static com.google.common.base.StandardSystemProperty.JAVA_IO_TMPDIR;
20+
import static com.google.common.truth.Truth.assertThat;
21+
import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
22+
import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
23+
import static org.junit.Assert.assertThrows;
24+
1925
import java.io.File;
2026
import java.io.IOException;
2127
import java.io.OutputStream;
28+
import java.nio.file.attribute.PosixFileAttributeView;
29+
import java.nio.file.attribute.PosixFileAttributes;
2230
import java.util.Arrays;
2331

2432
/**
@@ -61,10 +69,21 @@ private void testThreshold(
6169

6270
// Write data to go over the threshold
6371
if (chunk2 > 0) {
72+
if (JAVA_IO_TMPDIR.value().equals("/sdcard")) {
73+
assertThrows(IOException.class, () -> write(out, data, chunk1, chunk2, singleByte));
74+
return;
75+
}
6476
write(out, data, chunk1, chunk2, singleByte);
6577
file = out.getFile();
6678
assertEquals(dataSize, file.length());
6779
assertTrue(file.exists());
80+
assertThat(file.getName()).contains("FileBackedOutputStream");
81+
if (!isAndroid()) {
82+
PosixFileAttributes attributes =
83+
java.nio.file.Files.getFileAttributeView(file.toPath(), PosixFileAttributeView.class)
84+
.readAttributes();
85+
assertThat(attributes.permissions()).containsExactly(OWNER_READ, OWNER_WRITE);
86+
}
6887
}
6988
out.close();
7089

@@ -109,6 +128,10 @@ public void testWriteErrorAfterClose() throws Exception {
109128
FileBackedOutputStream out = new FileBackedOutputStream(50);
110129
ByteSource source = out.asByteSource();
111130

131+
if (JAVA_IO_TMPDIR.value().equals("/sdcard")) {
132+
assertThrows(IOException.class, () -> out.write(data));
133+
return;
134+
}
112135
out.write(data);
113136
assertTrue(Arrays.equals(data, source.read()));
114137

@@ -140,4 +163,8 @@ public void testReset() throws Exception {
140163

141164
out.close();
142165
}
166+
167+
private static boolean isAndroid() {
168+
return System.getProperty("java.runtime.name", "").contains("Android");
169+
}
143170
}

‎android/guava-tests/test/com/google/common/io/FilesCreateTempDirTest.java

+33-5
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,17 @@
1616

1717
package com.google.common.io;
1818

19+
import static com.google.common.base.StandardSystemProperty.JAVA_IO_TMPDIR;
1920
import static com.google.common.truth.Truth.assertThat;
21+
import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE;
22+
import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
23+
import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
24+
import static org.junit.Assert.assertThrows;
2025

2126
import java.io.File;
27+
import java.io.IOException;
28+
import java.nio.file.attribute.PosixFileAttributeView;
29+
import java.nio.file.attribute.PosixFileAttributes;
2230
import junit.framework.TestCase;
2331

2432
/**
@@ -27,12 +35,32 @@
2735
* @author Chris Nokleberg
2836
*/
2937

38+
@SuppressWarnings("deprecation") // tests of a deprecated method
3039
public class FilesCreateTempDirTest extends TestCase {
31-
public void testCreateTempDir() {
40+
public void testCreateTempDir() throws IOException {
41+
if (JAVA_IO_TMPDIR.value().equals("/sdcard")) {
42+
assertThrows(IllegalStateException.class, Files::createTempDir);
43+
return;
44+
}
3245
File temp = Files.createTempDir();
33-
assertTrue(temp.exists());
34-
assertTrue(temp.isDirectory());
35-
assertThat(temp.listFiles()).isEmpty();
36-
assertTrue(temp.delete());
46+
try {
47+
assertTrue(temp.exists());
48+
assertTrue(temp.isDirectory());
49+
assertThat(temp.listFiles()).isEmpty();
50+
51+
if (isAndroid()) {
52+
return;
53+
}
54+
PosixFileAttributes attributes =
55+
java.nio.file.Files.getFileAttributeView(temp.toPath(), PosixFileAttributeView.class)
56+
.readAttributes();
57+
assertThat(attributes.permissions()).containsExactly(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE);
58+
} finally {
59+
assertTrue(temp.delete());
60+
}
61+
}
62+
63+
private static boolean isAndroid() {
64+
return System.getProperty("java.runtime.name", "").contains("Android");
3765
}
3866
}

‎android/guava/src/com/google/common/io/FileBackedOutputStream.java

+14-8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package com.google.common.io;
1616

17+
import static com.google.common.base.Preconditions.checkArgument;
1718
import static java.util.Objects.requireNonNull;
1819

1920
import com.google.common.annotations.Beta;
@@ -36,6 +37,14 @@
3637
* An {@link OutputStream} that starts buffering to a byte array, but switches to file buffering
3738
* once the data reaches a configurable size.
3839
*
40+
* <p>When this stream creates a temporary file, it restricts the file's permissions to the current
41+
* user or, in the case of Android, the current app. If that is not possible (as is the case under
42+
* the very old Android Ice Cream Sandwich release), then this stream throws an exception instead of
43+
* creating a file that would be more accessible. (This behavior is new in Guava 32.0.0. Previous
44+
* versions would create a file that is more accessible, as discussed in <a
45+
* href="https://github.com/google/guava/issues/2575">Guava issue 2575</a>. TODO: b/283778848 - Fill
46+
* in CVE number once it's available.)
47+
*
3948
* <p>Temporary files created by this stream may live in the local filesystem until either:
4049
*
4150
* <ul>
@@ -61,7 +70,6 @@ public final class FileBackedOutputStream extends OutputStream {
6170
private final int fileThreshold;
6271
private final boolean resetOnFinalize;
6372
private final ByteSource source;
64-
@CheckForNull private final File parentDirectory;
6573

6674
@GuardedBy("this")
6775
private OutputStream out;
@@ -97,6 +105,7 @@ synchronized File getFile() {
97105
* {@link ByteSource} returned by {@link #asByteSource} is finalized.
98106
*
99107
* @param fileThreshold the number of bytes before the stream should switch to buffering to a file
108+
* @throws IllegalArgumentException if {@code fileThreshold} is negative
100109
*/
101110
public FileBackedOutputStream(int fileThreshold) {
102111
this(fileThreshold, false);
@@ -109,16 +118,13 @@ public FileBackedOutputStream(int fileThreshold) {
109118
* @param fileThreshold the number of bytes before the stream should switch to buffering to a file
110119
* @param resetOnFinalize if true, the {@link #reset} method will be called when the {@link
111120
* ByteSource} returned by {@link #asByteSource} is finalized.
121+
* @throws IllegalArgumentException if {@code fileThreshold} is negative
112122
*/
113123
public FileBackedOutputStream(int fileThreshold, boolean resetOnFinalize) {
114-
this(fileThreshold, resetOnFinalize, null);
115-
}
116-
117-
private FileBackedOutputStream(
118-
int fileThreshold, boolean resetOnFinalize, @CheckForNull File parentDirectory) {
124+
checkArgument(
125+
fileThreshold >= 0, "fileThreshold must be non-negative, but was %s", fileThreshold);
119126
this.fileThreshold = fileThreshold;
120127
this.resetOnFinalize = resetOnFinalize;
121-
this.parentDirectory = parentDirectory;
122128
memory = new MemoryOutput();
123129
out = memory;
124130

@@ -229,7 +235,7 @@ public synchronized void flush() throws IOException {
229235
@GuardedBy("this")
230236
private void update(int len) throws IOException {
231237
if (memory != null && (memory.getCount() + len > fileThreshold)) {
232-
File temp = File.createTempFile("FileBackedOutputStream", null, parentDirectory);
238+
File temp = TempFileCreator.INSTANCE.createTempFile("FileBackedOutputStream");
233239
if (resetOnFinalize) {
234240
// Finalizers are not guaranteed to be called on system shutdown;
235241
// this is insurance.

‎android/guava/src/com/google/common/io/Files.java

+19-30
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,6 @@
7373
@ElementTypesAreNonnullByDefault
7474
public final class Files {
7575

76-
/** Maximum loop count when creating temp directories. */
77-
private static final int TEMP_DIR_ATTEMPTS = 10000;
78-
7976
private Files() {}
8077

8178
/**
@@ -399,17 +396,19 @@ public static boolean equal(File file1, File file2) throws IOException {
399396
* Atomically creates a new directory somewhere beneath the system's temporary directory (as
400397
* defined by the {@code java.io.tmpdir} system property), and returns its name.
401398
*
399+
* <p>The temporary directory is created with permissions restricted to the current user or, in
400+
* the case of Android, the current app. If that is not possible (as is the case under the very
401+
* old Android Ice Cream Sandwich release), then this method throws an exception instead of
402+
* creating a directory that would be more accessible. (This behavior is new in Guava 32.0.0.
403+
* Previous versions would create a directory that is more accessible, as discussed in <a
404+
* href="https://github.com/google/guava/issues/4011">CVE-2020-8908</a>.)
405+
*
402406
* <p>Use this method instead of {@link File#createTempFile(String, String)} when you wish to
403407
* create a directory, not a regular file. A common pitfall is to call {@code createTempFile},
404408
* delete the file and create a directory in its place, but this leads a race condition which can
405409
* be exploited to create security vulnerabilities, especially when executable files are to be
406410
* written into the directory.
407411
*
408-
* <p>Depending on the environment that this code is run in, the system temporary directory (and
409-
* thus the directory this method creates) may be more visible that a program would like - files
410-
* written to this directory may be read or overwritten by hostile programs running on the same
411-
* machine.
412-
*
413412
* <p>This method assumes that the temporary volume is writable, has free inodes and free blocks,
414413
* and that it will not be called thousands of times per second.
415414
*
@@ -418,36 +417,26 @@ public static boolean equal(File file1, File file2) throws IOException {
418417
*
419418
* @return the newly-created directory
420419
* @throws IllegalStateException if the directory could not be created
420+
* @throws UnsupportedOperationException if the system does not support creating temporary
421+
* directories securely
421422
* @deprecated For Android users, see the <a
422423
* href="https://developer.android.com/training/data-storage" target="_blank">Data and File
423424
* Storage overview</a> to select an appropriate temporary directory (perhaps {@code
424-
* context.getCacheDir()}). For developers on Java 7 or later, use {@link
425-
* java.nio.file.Files#createTempDirectory}, transforming it to a {@link File} using {@link
426-
* java.nio.file.Path#toFile() toFile()} if needed.
425+
* context.getCacheDir()}), and create your own directory under that. (For example, you might
426+
* use {@code new File(context.getCacheDir(), "directoryname").mkdir()}, or, if you need an
427+
* arbitrary number of temporary directories, you might have to generate multiple directory
428+
* names in a loop until {@code mkdir()} returns {@code true}.) For developers on Java 7 or
429+
* later, use {@link java.nio.file.Files#createTempDirectory}, transforming it to a {@link
430+
* File} using {@link java.nio.file.Path#toFile() toFile()} if needed. To restrict permissions
431+
* as this method does, pass {@code
432+
* PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))} to your
433+
* call to {@code createTempDirectory}.
427434
*/
428435
@Beta
429436
@Deprecated
430437
@J2ObjCIncompatible
431438
public static File createTempDir() {
432-
File baseDir = new File(System.getProperty("java.io.tmpdir"));
433-
@SuppressWarnings("GoodTime") // reading system time without TimeSource
434-
String baseName = System.currentTimeMillis() + "-";
435-
436-
for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) {
437-
File tempDir = new File(baseDir, baseName + counter);
438-
if (tempDir.mkdir()) {
439-
return tempDir;
440-
}
441-
}
442-
throw new IllegalStateException(
443-
"Failed to create directory within "
444-
+ TEMP_DIR_ATTEMPTS
445-
+ " attempts (tried "
446-
+ baseName
447-
+ "0 to "
448-
+ baseName
449-
+ (TEMP_DIR_ATTEMPTS - 1)
450-
+ ')');
439+
return TempFileCreator.INSTANCE.createTempDir();
451440
}
452441

453442
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2019 The Guava Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions and limitations under
12+
* the License.
13+
*/
14+
15+
package com.google.common.io;
16+
17+
import static java.lang.annotation.ElementType.CONSTRUCTOR;
18+
import static java.lang.annotation.ElementType.METHOD;
19+
import static java.lang.annotation.ElementType.TYPE;
20+
21+
import java.lang.annotation.Target;
22+
23+
/**
24+
* Disables Animal Sniffer's checking of compatibility with older versions of Java/Android.
25+
*
26+
* <p>Each package's copy of this annotation needs to be listed in our {@code pom.xml}.
27+
*/
28+
@Target({METHOD, CONSTRUCTOR, TYPE})
29+
@ElementTypesAreNonnullByDefault
30+
@interface IgnoreJRERequirement {}

0 commit comments

Comments
 (0)